التطبيقات: الاعتماديات والدورات
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
So far you have explored graphs with BFS and DFS. This lesson puts the tools to work on the graph that backend engineers meet most often: the dependency graph. Build tools, package managers, and service startup scripts all hold facts of the form "A needs B". Draw each fact as a directed edge and you have a directed graph.
Two questions matter on such a graph. First, is there a valid order in which to build or start everything, so that each thing comes after what it needs? Second, if not, why not? The answer to the second is a cycle: "A needs B, B needs A". DFS detects cycles and, as a bonus, produces a valid order when there is none.
We will also revisit BFS for a networks-and-routes question: "how many hops, and who is affected". All code was compiled and run with JDK 21.
Why this matters: a cycle in dependencies is a real production failure, from a build that cannot start to services that wait on each other forever. Detecting it early, with a few lines of DFS, is cheap.
This lesson builds on "Depth-first search", on directed adjacency lists from "Representing graphs", and on recursion.
Learning Objectives
By the end of this lesson you will be able to:
- Model "A needs B" relationships as a directed graph.
- Explain why a cycle means there is no valid build order.
- Detect a directed cycle with DFS by tracking the nodes on the current path.
- Produce a build order, with dependencies before dependents, from DFS.
- Explain why meeting an already-visited node is not always a cycle.
- Say when BFS, not DFS, is the right tool for networks and routes.
Written Explanation
Dependencies are a directed graph
Suppose an application needs a web library and a database library, and both of those need a core library. Write each "needs" as an edge from the dependent to what it depends on:
app -> web
app -> db
web -> core
db -> coreIn code this is the directed adjacency list from Lesson 2. We will use a helper that records "a needs b" and registers both nodes:
static Map<String, List<String>> g = new LinkedHashMap<>();
static void needs(String a, String b) {
g.computeIfAbsent(a, k -> new ArrayList<>()).add(b);
g.computeIfAbsent(b, k -> new ArrayList<>());
}The map is a LinkedHashMap so that loops over its keys are in the order we added them, which keeps the output of the examples predictable.
A valid build order puts every library after the libraries it needs. For this graph, core, then web and db in either order, then app.
Why a cycle is a dead end
Add one more fact: core -> app, "core needs app". Now app needs web, web needs core, and core needs app. Starting anywhere, each library waits for another that waits for it. No library can go first, so no valid order exists. A cycle in a dependency graph means the build or startup cannot be done. The only fix is to change the design (usually by extracting the shared piece into a new library that the others both need).
Detecting a cycle with DFS
DFS walks along a path. A directed cycle means that, somewhere along the current path, an edge leads back to a node that is still on the path. So we track two sets:
onPath: the nodes we have entered and not yet finished. These are the nodes of the current recursion path.done: nodes whose exploration has completely finished.
static boolean visit(String n, Set<String> done, Set<String> onPath) {
if (onPath.contains(n)) return true; // back to a node on our path: cycle
if (done.contains(n)) return false; // already fully explored: safe
onPath.add(n);
for (String m : g.get(n)) {
if (visit(m, done, onPath)) return true;
}
onPath.remove(n);
done.add(n);
return false;
}
static boolean hasCycle() {
Set<String> done = new HashSet<>();
Set<String> onPath = new HashSet<>();
for (String n : g.keySet()) {
if (visit(n, done, onPath)) return true;
}
return false;
}hasCycle loops over every node because the graph may have several pieces, and a cycle can hide in a piece you have not visited yet. The same idea appears in algorithm references in the language of three colors: white for unvisited, gray for "entered but not finished" (on the current path), and black for finished. Reaching a gray node signals a cycle. onPath is gray and done is black.
Why "visited" alone is not enough
Beginners often detect a cycle with the plain visited set from the last lesson: "if I reach a node I have already seen, there is a cycle". That is wrong for directed graphs. In our dependency graph, web and db both need core. DFS reaches core through web, finishes it, then reaches it again through db. The second arrival is not a cycle: core is finished, not on the path. A cycle needs the edge to return to a node still on the path. This is why two sets are needed.
A build order from DFS
DFS can also produce the build order. The idea: add a node to the output list when its exploration is finished, that is after all the things it needs have been added.
static List<String> buildOrder() {
List<String> out = new ArrayList<>();
Set<String> seen = new HashSet<>();
for (String n : g.keySet()) post(n, seen, out);
return out;
}
static void post(String n, Set<String> seen, List<String> out) {
if (!seen.add(n)) return;
for (String m : g.get(n)) post(m, seen, out);
out.add(n);
}Because a library is added only after all its dependencies were added, every dependency appears before its dependents. This is a standard use of DFS: ordering vertices by finish time yields a topological order, which exists only when the graph has no cycles. The code above assumes no cycle, so run hasCycle first.
Networks and routes: back to BFS
Not every graph application needs cycles. In a network of machines, or a route map, the common question is "how many hops?" That is BFS, which you know. The two tools split the work:
- DFS: cycles, build order, "can I get there at all".
- BFS: fewest hops, "what is within k steps".
For a service graph, both appear together: DFS to check that startup order is possible, BFS to measure how far a failure can spread.
Examples
Example 1: a valid build order (simple)
needs("app", "web"); needs("app", "db"); needs("web", "core"); needs("db", "core");
System.out.println(hasCycle());
System.out.println(buildOrder());Output:
false
[core, web, db, app]No cycle, and the order puts core first, app last. Check by hand: buildOrder starts at app, goes to web, then core, which finishes first. Then web finishes, then db (its core is already seen), then app.
Example 2: the cycle that blocks the build (practical)
Starting from the same graph, add needs("core", "app"):
needs("core", "app");
System.out.println(hasCycle());Output:
trueTrace it: visit(app) puts app on the path, goes to web, then core, then finds the edge to app. App is on the path, so it returns true. The cycle is app, web, core, app. For a build tool this is the point where it should refuse to start and print the cycle, not hang. A fresh three-library loop, needs("A","B"); needs("B","C"); needs("C","A");, also printed true.
Example 3: the diamond that is not a cycle (realistic)
Four libraries: app needs a and b, and both a and b need c. This "diamond" shape is extremely common, since two libraries often share one underlying library.
needs("app","a"); needs("app","b"); needs("a","c"); needs("b","c");
System.out.println(hasCycle() + " " + buildOrder());Output:
false [c, a, b, app]No cycle. I also ran the wrong version, where any node seen twice counts as a cycle (a single visited set), on this same graph, and it returned true. A tool built that way would reject every diamond, which means almost every real project. The onPath set is what tells "reached again" apart from "looped back".
Backend Insight
Dependency graphs run through the daily work of a backend engineer. A package manager must install libraries before the ones that need them. A build system must compile modules in a valid order. A deployment may need the database before the service, and the service before the gateway. In each case, a cycle is a hard error, and good tools detect it and report the loop so a human can fix it.
Service graphs add the second skill. When one service slows down, the services that call it, and the ones that call those, are at risk. Following the arrows backward with BFS gives the blast radius in hops. Teams that draw and review these graphs often find hidden cycles: two services that call each other can wait on each other under load.
Later in the roadmap, dependency mapping comes back as a professional skill, and the same graph vocabulary will help you read it.
Common Mistakes
Mistake 1: Treating "seen twice" as a cycle
In a directed graph, a shared dependency (the diamond) is seen twice without any cycle. Track the nodes on the current path separately from finished ones.
Mistake 2: Checking only one start node
A dependency graph can have several unconnected pieces. Loop over all nodes, skipping finished ones, as hasCycle does.
Mistake 3: Forgetting to remove the node from the path
onPath.remove(n) after the loop is essential. If you leave nodes on the path, finished branches look like they are still active, and a harmless diamond is reported as a cycle.
Mistake 4: Running buildOrder on a graph with a cycle
The result is a list that is not a valid order, with no warning. Always run hasCycle first, or detect the cycle in the same pass.
Mistake 5: Reversing the meaning of the edges
If edges mean "needs", the build order puts targets first. If you define edges as "must come before", the list must be read the other way. Decide the meaning, write it down, and check one small example.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Does this "needs" graph have a cycle? X -> Y, Y -> Z, Z -> X.
أظهر الحل A1
A1. Yes: X, Y, Z, X.
A2. Does this one? app -> a, app -> b, a -> c, b -> c. Explain in one sentence.
أظهر الحل A2
A2. No. Both a and b reach c, but c is a shared dependency, not a node on the current path when the second arrival happens.
A3. For app -> lib and lib -> core, write a valid build order.
أظهر الحل A3
A3. core, lib, app.
Level B: Application
B1. Edges: P -> Q, P -> R, Q -> S, R -> S, S -> T. Give a valid build order. Follow the DFS rule from buildOrder, starting at P with neighbors in the order Q, R.
أظهر الحل B1
B1. buildOrder from P: go to Q, then S, then T. T finishes first, then S, then Q. Then R (S already seen) finishes, then P. Order: T, S, Q, R, P. I ran this and got [T, S, Q, R, P]. Every library appears after the ones it needs. Swapping Q and R would also be valid.
B2. Add the edge S -> P to B1. Is there now a cycle? Name it.
أظهر الحل B2
B2. Yes: P -> Q -> S -> P (also P -> R -> S -> P). With the cycle there is no valid build order.
B3. Write a method dependents(String lib) that returns the libraries that directly need lib. Use g.entrySet().
أظهر الحل B3
B3.
static List<String> dependents(String lib) {
List<String> r = new ArrayList<>();
for (Map.Entry<String, List<String>> e : g.entrySet()) {
if (e.getValue().contains(lib)) r.add(e.getKey());
}
return r;
}On the diamond graph from Example 3, dependents("c") returned [a, b].
Level C: Challenge
C1. Service calls: Gateway -> Orders, Gateway -> Users, Orders -> Payments, Orders -> Users, Payments -> Bank. If Bank fails, which services are affected, directly or indirectly? Which traversal would you write, and on which direction of edges?
أظهر الحل C1
C1. Payments calls Bank directly, Orders calls Payments, and Gateway calls Orders. So Payments, Orders, and Gateway are affected. Users is not. Use BFS following edges backward: from Bank to whoever calls it, then to whoever calls those. Either build the reverse graph or use something like dependents repeatedly. BFS also gives the hop count: Payments 1, Orders 2, Gateway 3.
C2. Explain why the cycle check above would be wrong for an undirected graph if copied unchanged. Hint: what does an undirected edge look like from the other end?
أظهر الحل C2
C2. In an undirected graph each edge appears in both lists. From A you go to B, and B's list contains A, which is on the path, so the check would report a cycle for every single edge. A correct undirected check must ignore the edge back to the node you just came from (the parent). This is a standard fix; I did not run code for this one.
Mini Challenge
Model the startup order of a small system: gateway needs orders and users; orders needs db and cache; users needs db; cache needs nothing.
- Write
needscalls for it and printhasCycle()andbuildOrder(). - Check by hand that every service appears after what it needs.
- Add one change that creates a cycle (for example
dbneedsgateway) and show thathasCycle()becomes true. - Rebuild the graph without the cyclic edge, and instead let
dbandgatewayboth need a new nodeconfig. Show thathasCycle()is false again.
Check your work: the first order has db and cache before orders, the cyclic version returns true, and the fixed version returns false.
Summary & Next Lesson
Key takeaways
- Dependencies are a directed graph: "A needs B" is an edge from A to B.
- A cycle means there is no valid build or startup order.
- Detect a directed cycle with DFS by tracking the nodes on the current path. Reaching one of them again is a cycle; reaching a finished node is not.
- Adding a node to a list when its DFS call finishes gives an order with dependencies first, if there is no cycle.
- For "how many hops" and "who is affected", use BFS.
Next lesson
The next lesson, "Demo: metro route finder with BFS", is a hands-on build. It finds the fewest-stops route between two stations, rebuilds the route from a "came from" map, and handles unreachable stations. It prepares the module lab.
Self-check
- What does an edge mean in a "needs" graph?
- Why does a cycle in dependencies mean there is no valid order?
- What must DFS track to detect a directed cycle?
- Is reaching an already finished node a cycle?
- Which traversal measures hops, BFS or DFS?
أظهر الإجابات
- It points from a thing to something it needs.
- Each item in the cycle needs another one in it, so none can come first.
- The nodes on the current path, in addition to finished nodes.
- No. Only returning to a node still on the current path is a cycle.
- BFS.
References
- CP-Algorithms, Finding a cycle in a graph. Supports the white, gray, black scheme for directed cycle detection, where reaching a gray node (still on the current recursion path) means a cycle.
- CP-Algorithms, Topological Sorting. Supports that a topological order exists only for directed graphs with no cycles. The DFS finish-order construction is described there in terms of ordering by exit time; this lesson's version appends on finish, which gives dependencies first for "needs" edges.
- CP-Algorithms, Depth First Search. Lists topological sorting and cycle detection among DFS applications.
- Everything about build tools, package managers, and service failures is described at a general level as common practice; I did not fetch a source for specific tools. The undirected-cycle remark in C2 is a standard idea I did not verify against a source. All Java code was compiled and run with JDK 21.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.