I have a list of items that have a partial order relation, i. e, the list can be considered a partially ordered set. I want to sort this list in the same way as in this question. As correctly answered there, this is known as topological sorting.
There's a reasonably simple known algorithm to solve the problem. I want a LINQ-like implementation of it.
I already tried to use OrderBy
extension method, but I'm quite sure it's not able to make topological sorting. The problem is that the IComparer<TKey>
interface is not able to represent a partial order. This happens because the Compare
method can return basically 3 kinds of values: zero, negative, and positive, meaning are-equal, is-less-than, and is-greater-then, respectively. A working solution would only be possible if there were a way to return are-unrelated.
From my biased point of view, the answer I'm looking for might be composed by an IPartialOrderComparer<T>
interface and an extension method like this:
public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IPartialOrderComparer<TKey> comparer
);
How would this be implemented? How does the IPartialOrderComparer<T>
interface would look like? Would you recommend a different approach? I'm eager to see it. Maybe there's a nicer way to represent the partial order, I don't know.
Well, I'm not sure that this way of handling it is the best way, but I could be wrong.
The typical way to handle topological sorting is by using a graph, and for each iteration remove all nodes which doesn't have an inbound connection, and simultaneously remove all connections outbound from those nodes. The removed nodes are the output of the iteration. Repeat until you cannot remove any more nodes.
However, in order to get those connections in the first place, with your method, you would need:
In other words, the method would probably defined like this:
and then return
null
when it doesn't have a conclusive answer for two keys.The problem I'm thinking about is the "iterate all combinations" part. Perhaps there's a better way to handle this, but I'm not seeing it.
thanks very much to everybody, starting from Eric Mickelsen's answer I've came up with my version as I prefer to use null values to indicate no relation as Lasse V. Karlsen said.
then I have the following interface for the comparer
and this helper class
that allows to beautify the usage a bit so my tests can looks like the following
This is my optimized and refurbished version of tehMick's answer.
One change I made was replacing the real list of values left to yield for a logical list. For this, I have two size arrays of the same size. One has all the values, and the others contains flags telling if each value has been yielded. This way, I avoid the cost of having to resize a real
List<Key>
.One other change is that I'm reading all keys only one time at the start of the iteration. For reasons I can't recall now (maybe it' was just my instinct) I don't like the idea of calling the
keySelector
function several times.The last touches was parameter validation, and an extra overload that uses an implicit key comparer. I hope the code is readable enough. Check it out.
I believe that Lasse V. Karlsen's answer is on the right track, but I don't like hiding of the Compare method (or a separate interface for that matter which doesn't extend from
IComparable<T>
).Instead, I'd rather see something like this:
This way, you still have an implementation of
IComparer<T>
which can be used in other places that requireIComparer<T>
.However, it also requires you to indicate the relationship of the instances of T to each other with the return value in the following way (similar to
IComparable<T>
):Of course, you won't get partial ordering when passing an implementation of this to anything that expects an
IComparable<T>
(and it should be noted that Lasse V. Karlsen's answer doesn't solve this either) as what you need can't be represented in a simple Compare method which takes two instances of T and returns an int.To finish the solution, you have to provide a custom OrderBy (as well as ThenBy, OrderByDescending and ThenByDescending as well) extension method which will accept the new instance parameter (as you have already indicated). The implementation would look something like this:
I would suggest using the same IComparer interface, but writing the extension method so as to interpret 0 as not related. In a partial ordering, if elements a and b are equal their order doesn't matter, like-wise if they are unrelated - you only have to order them with respect to elements with which they have defined relationships.
Here's an example that does a partial ordering of even and odd integers:
Result: 4, 8, 3, 5, 7, 10
Interface to define partial order relationship:
Compare
should return-1
ifx < y
,0
ifx = y
,1
ify < x
andnull
ifx
andy
are not comparable.Our goal is to return an ordering of the elements in the partial order that respects the enumeration. That is, we seek a sequence
e_1, e_2, e_3, ..., e_n
of the elements in the partial order such that ifi <= j
ande_i
is comparable toe_j
thene_i <= e_j
. I'll do this using a depth-first search.Class that implements topological sort using depth-first search:
Helper class needed for marking nodes as visited while doing depth-first search:
I make no claim that this is the best implementation of the algorithm but I believe that it is a correct implementation. Further, I did not return an
IOrderedEnumerable
as you requested but that is easy to do once we are at this point.The algorithm works by doing a depth-first search through the elements adding an element
e
to the linear ordering (represented by_sorted
in the algorithm) if we have already added all the predecessors ofe
have already been added to the ordering. Hence, for each elemente
, if we haven't already visited it, visit its predecessors and then adde
. Thus, this is the core of the algorithm:As an example, consider the partial-ordering defined on subsets of
{1, 2, 3}
whereX < Y
ifX
is a subset ofY
. I implement this as follows:Then with
sets
defined as the list of subsets of{1, 2, 3}
This results in the ordering:
which respects the partial order.
That was a lot of fun. Thanks.