The array approach is based on
Kahn's algorithm, and the array does contain the packages sorted according to increasing number of unfulfilled dependencies. It is the fact that finding a specific package by its ID is slow that makes it not very good for identifying packages in a loop. It is easy to iterate over all packages, though.
Because of that, I think I personally would prefer a different representation (but same logic and algorithm), perhaps
typedef struct pkg_deps { pkg_deps *next; // Main list pkg_deps *stack; // Mutable stack list during searches uint32_t id; // Numeric identifier uint32_t tag:8; // Loop detection mutable tag uint32_t deps:24; // Number of dependencies left in dep[] pkg_deps *dep[]; // Pointers to pkg_deps } pkg_deps;This is pointer-size word aligned, and can be stored in an array or pool of such words; there is no need to allocate each one dynamically. The length does vary: the
dep[] member is a C99 or later flexible array member. I'm not sure how myC would represent the same structure, or whether it is possible, though. The pointer to each such structure, or its location, will not change after it has been added, though.
You use two lists to maintain the graph: one with packages with
deps==0, and the other with
deps>0.
Overall, for
P packages with
D dependencies total, this uses 16*
P+4*
D+8 bytes on 32-bit architectures, and 24*
P+8*
D+16 bytes on 64-bit architectures. For
P=1024,
D<28670 on 32-bit architectures this takes less memory than the bitmap dependency matrix, and
D<13310 on 64-bit architectures. That means if you have 1024 packages or more, and each package has fewer than 28 (32-bit) or 13 (64-bit) dependencies on average, this is more compact than the bitmap matrix. For
P=2048,
D<59390, so on 64-bit, with 2048 packages or more with less than 29 dependencies on average, this is more compact. So, if you have a thousand or more packages, this is likely to be more compact than the bitmap dependency matrix is.
(I might replace
id with
pkg_desc *desc;, a pointer to the package description; and change
tag and
deps to
uint32_t, reducing the cost of clearing
tag but increasing the memory use a small bit.)
Applying Kahn's algorithm, you can pick any package from the
deps==0 list to install. When it is empty, but the other list is not, you have a loop.
The
stack member is used during a search to form the last-in-first-out or LIFO stack of nodes to visit. The handle to this list is kept in one or two global variables, or local variables in a loop-based iterator.
The
tag member is used for the
seen and
visited flags. You have two global variables,
seen and
visited, initialized to 253 and 254, respectively. Before starting a new search, you increment both by two. If
visited==256 or 0 (the larger overflows or wraps), you reset them to 1 and 2, respectively, and traverse both lists to clear all
tag members to zero. This way, the cost of clearing the two flags for all packages is amortized among many (127) searches.
For a depth-first search, you use a LIFO stack, inserting all
dep[] nodes neither
seen nor
visited yet to the beginning of the stack list, of course marking each
seen.
For a breadth-first search, you use a FIFO stack, appending all
dep[] nodes neither
seen nor
visited yet to the end of the stack list, marking each
seen. In addition to the handle to the stack list, you maintain a pointer to the final node in the stack list, to avoid having to traverse the stack list for appending.
When you pop off the next node in the stack list, you first need to check if a previous iteration has
visited it already. With the stack list, all searches start at a specific node, which you put as the only element to the stack, and tag it
seen. You then iterate over a loop, popping off a node from the stack. If it has been
visited already, you skip the iteration and continue with the next node off the stack. You tag the node
visited, and check all its
dep[]s. A
visited dep[] indicates a loop, so a loop-finding iterator can just return that node. A set-finding iterator adds the popped-off node to the result stack, and tags all non-
visited and non-
seen dep[]s
seen and adds them to the stack.
Technically, a simple loop is not what we want to extract.What we really want, is the (preferably smallest) interconnected set left in the graph: not just the packages in the smallest loop, but all their dependencies too.
We know that there are no immediately fulfillable packages left when the
m=0 list is empty, but you can still have connected rings, like A-B-C depending on each other, D-E-F depending on each other, but C and D also depending on each other –– i.e. A:B B:C C:AD D:CE, E:F, F:A.
If you only report A-B-C or D-E-F, the user is still hosed, because the loop itself is not self-contained. Because of this, we actually need a two-part process: one is to find the/a loop, and the other is to collect all the dependencies for the packages involved in the loop.
Using the structure referred to above, a DFS will detect a loop when any of the nodes in
dep[] are already visited. That particular node is then known to be part of a loop, and is the starting point for the search for the whole connected set. Such a search has to be done for each package left, until a loop is found.
(If you consider the above letter example, we might have started from package G that depends only on one of the packages in the interconnected set. However, because we detect the loop because something in the loop already depends on the particular node, we know it has to be part of a loop.)
The entire dependency tree is then traversed starting at that particular node. The
stack member then has double use: when popping off nodes from the stack when visiting them, they are added to the resulting interconnected set list.
Detaching the entire interconnected set from the graph is done normally, one package at a time. The package itself is removed from its list, and all lists are traversed removing it from the
dep[] lists (by swapping the pointer with the final one, and decrementing
deps). Whenever
deps changes, the package is moved to a different list (unless
deps >=
M).
(Note that in the A-G example, G would
not be considered part of the interconnected set, because nothing in the loop directly or indirectly depends on G.)
I have not verified this suggestion works, but I do believe it should. Let me know if I have forgotten or mistaken a detail, though!