My take:
genIdx[n_?EvenQ] :=
Flatten@With[{r = Range@n}, Fold[With[{l1 = #, l2 = #2},
Flatten[Map[With[{la = #, c = Complement[r, #]},
Join[la, c[[#]]] & /@ l2] &, l1], 1]] &,
Subsets[Range@#, {2}, # - 1] & /@ Range[#, 2, -2] &@n]];
xformLst[lst_, idx_] :=
If[(Length@lst*(Length@lst - 1)!!) == Length@idx,
With[{l = Length@lst}, ArrayReshape[lst[[idx]], {(l - 1)!!, l/2, 2}]], Abort[]];
Use examples:
source = {x1, x1, x3, x4}
xformLst[source, genIdx@Length@source]
(* {{{x1, x1}, {x3, x4}}, {{x1, x3}, {x1, x4}}, {{x1, x4}, {x1, x3}}} *)
On its own, including genIdx time, this was ~15X faster than fastest answer so far on a list of length 14 (caveats as usual for loungebook performance).
However, the prime benefit is amortization of time over multiple lists to be transformed (I assume you're doing this for more than some one-shot). One simply uses genIdx to generate and save the index set(s) for the size(s) of lists to be transformed, once, and supply those to the transformation function, e.g.
source = {x1, x1, x3, x4};
idx4= genIdx@4;
result=xformLst[source,idx4];
Using this for tests against multiple randomly generated lists of length 14 showed it to be ~500X faster than the fastest answer so far posted...
Only rudimentary error checking is done (e.g., even length argument, will abort if called with list length not matching required index list) - season as desired.
Update: An explanation of what's going on...
I viewed this as a problem of getting the most efficient way of picking items from the source list in the needed order. I chose to do this as a flattened index, since nearly always grabbing with something like list[[{n1,n2,...nm}]]
is faster than alternatives like say Map[list[[#]]&,{n1,n2,...}]
and the like.
Let's use an example case of lists of length 6. For the output the OP is after, that means the first two positions picking for the first pair look like this:
{{1, 1, 0, 0, 0, 0}, {1, 0, 1, 0, 0, 0}, {1, 0, 0, 1, 0, 0},
{1, 0, 0,0, 1, 0}, {1, 0, 0, 0, 0, 1}}
You'll note that is precisely described by
Subsets[Range@6, {2}, 5]
(* {{1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6}} *)
For the next two positions to pick, we can only fill places not already occupied. So, we treat those as lists of length 4, meaning the picks look like
{{1, 1, 0, 0}, {1, 0, 1, 0}, {1, 0, 0, 1}}
and are precisely described by
Subsets[Range@4, {2}, 3]
(* {{1, 2}, {1, 3}, {1, 4}} *)
This continues until there are only two empty slots left, whatever the length of the source list.
This cascade of subset results forms the basis for our work, and is built via the part of the code
Subsets[Range@#, {2}, # - 1] & /@ Range[#, 2, -2] &@n
So for the length 6 example, we'd have a basis of
{{{1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6}}, {{1, 2}, {1, 3}, {1, 4}}, {{1, 2}}}
N.b.: the #-1
term that reduces the length of the subsets returned hides a pretty relationship: we can extend this method to arbitrary sizes (triplets, etc.) by using the appropriate values for cut-offs there, and these are just the appropriate order figurate numbers.
Once we have the basis, we Fold over that list. For each sub-result, we derive what actual position(s) are available for that round (by Complementing the positions already taken with the span of possible positions), and then select from those using the appropriate subset(s) of those positions.
The end result is then just Flattened out to increase efficiency at picking the items from the list in the needed order, the result of which is then reshaped into the final form.