This is really a graph problem! Draw a graph with a vertex for every number in your list, and an edge between vertices \$a\$ and \$b\$ if there is a pair \$(a, b)\$ or \$(b, a)\$ in your list:
What you want to do is to find a path in this graph that visits all the edges. Such a path is known as an Eulerian path. Your condition that each number appears exactly twice in the list means that the path must return to the original vertex: that makes it an Eulerian cycle.
To find this cycle, we must first represent the graph using a suitable data structure. Here we can use the adjacency list representation (really, adjacency set):
from collections import defaultdict
def graph(edges):
"""Given a list of edges, return a map from vertex to a set of
adjacent vertices.
>>> graph([(1, 2), (1, 3), (3, 4), (2, 4)])
defaultdict(<class 'set'>, {1: {2, 3}, 2: {1, 4}, 3: {1, 4}, 4: {2, 3}})
"""
g = defaultdict(set)
for v, w in edges:
g[v].add(w)
g[w].add(v)
return g
Then, to find the path, we can use Hierholzer's algorithm. This is really simple: we just pick a starting vertex and then repeatedly pick any unvisited edge from that vertex. Your condition that each number appears exactly twice in the list of edges means that the algorithm can't get stuck: there are exactly two edges meeting each vertex, so having gone in on one we can go out on the other:
def eulerian_path(edges, v):
"""Generate an Eulerian path over the given collection of edges,
starting at the vertex v.
>>> list(eulerian_path([(1, 2), (1, 3), (3, 4), (2, 4)], 1))
[(1, 2), (2, 4), (4, 3), (3, 1)]
"""
g = graph(edges)
while True:
try:
w = g[v].pop()
except KeyError:
return
g[w].remove(v)
yield v, w
v = w
What might happen, however, is that your data contains multiple disjoint cycles. For example if the input is:
[(1, 2), (2, 3), (3, 1), (4, 5), (5, 6), (6, 4)]
then this corresponds to the graph:
The eulerian_path
algorithm only finds one of the cycles. What should you do in ths case? Stop at the end of the first cycle? Keep going and find all the other cycles? Maybe this can't happen in your problem, so raise an error? You didn't explain what your problem really is, so I can't tell you what the right thing is to do in this case.
Update
If you're determined to stick to NumPy, then consider using the Compressed Sparse Graph Routines. The idea is to use scipy.sparse.csr_matrix
to convert your array into a CSR matrix:
>>> edges = np.array([(1, 2), (1, 3), (3, 4), (2, 4)])
>>> m = scipy.sparse.csr_matrix((np.ones(len(edges)), (edges[:,0], edges[:,1])), shape=(5, 5))
and then call scipy.sparse.csgraph.depth_first_order
to get the vertices on the Eulerian path:
>>> scipy.sparse.csgraph.depth_first_order(m, 1, False)[0]
array([1, 2, 4, 3], dtype=int32)
If you need to turn this array of vertices back into an array of edges (as in the OP), you could use numpy.vstack
and numpy.roll
, like this:
>>> np.vstack((vertices, np.roll(vertices, -1))).T
array([[1, 2],
[2, 4],
[4, 3],
[3, 1]], dtype=int32)