Skip to content

fix: build OrderedSet/OrderedCollection/OrderedMap in O(n log n) and stop OrderedSet set operations going quadratic [patch] - #111

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/issue-82-bulk-sort
Oct 8, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/issue-82-bulk-sort

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #82

Summary

The sorted containers' bulk constructors filled the backing List<T> one binary-search Insert at a time, so building from n unsorted or reverse-sorted items was O(n²). OrderedSet.ToComparerSet goes through that constructor, which made IsSubsetOf, IsProperSubsetOf, IsProperSupersetOf, SetEquals, IntersectWith and SymmetricExceptWith quadratic in the size of the argument.

  • New StableSort (internal): a top-down merge sort with an insertion-sort cutoff. It takes from the left half on ties, so it is stable, and it skips the merge when two halves are already in order, so sorted input (such as Clone) costs O(n).
  • OrderedSet(IEnumerable<T>[, comparer]): copies, stable-sorts, then drops adjacent equal elements and keeps the first, which is what repeated Add calls did.
  • OrderedCollection(IEnumerable<T>[, comparer]): copies and stable-sorts, so equal elements keep their input order, as Add (upper-bound insert) does.
  • OrderedMap(IDictionary[, comparer]): copies entries, checks for null keys, stable-sorts by key, and throws the same ArgumentException (same message) that Add throws when two keys compare equal under the comparer.
  • OrderedSet set operations:
    • ToComparerSet now reuses an OrderedSet<T> argument that has the same comparer instead of copying it.
    • IntersectWith compacts in one pass with RemoveAll instead of calling RemoveAt once per removed element.
    • SymmetricExceptWith now does a single linear merge of the two sorted lists. The old code made repeated RemoveAt, Remove and Add calls, each of which is O(n). A call on the set itself still empties it, and an OrderedSet argument is no longer modified.
    • IsSupersetOf and Overlaps are unchanged.

#80 is not addressed beyond what this naturally covers.

Testing

New tests:

  • Performance, using a Stopwatch with a 5 s budget on 1,000,000 elements:
    • construction from descending input for OrderedSet, OrderedCollection and OrderedMap
    • OrderedSet.IsSubsetOf, SetEquals, IntersectWith and SymmetricExceptWith against a descending array
  • Semantics:
    • OrderedSet keeps the first occurrence of duplicates, using 1,000 items with a key comparer so the merge path runs.
    • OrderedSet honours a case-insensitive comparer and a reverse comparer.
    • Bulk construction matches repeated Add on random input.
    • OrderedMap throws ArgumentException on keys that compare equal, matching Add, and honours a custom comparer.
    • OrderedCollection keeps equal elements in insertion order, using 1,000 items so the merge path runs.
    • SymmetricExceptWith works on the set itself and on an OrderedSet with the same comparer.

Revert proof: I stashed only the three production files (OrderedSet.cs, OrderedCollection.cs, OrderedMap.cs) and ran the new tests. All 7 performance tests failed their budget. The OrderedSet/OrderedCollection cases took about 73–98 s and the OrderedMap constructor took about 174 s, against a 5 s budget. The semantics tests passed on both paths, as expected, because they check behaviour that already existed. With the fix restored, all of them pass, and the whole suite runs in about 4 s.

Full suite: dotnet test reports 470/470 passed. dotnet build -c Release reports 0 warnings and 0 errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K6bDUGMFsAQews3TnA2jXr


Generated by Claude Code

…stop OrderedSet set operations going quadratic [patch]

The bulk constructors filled the backing list one binary-search insert at a
time, shifting the list tail on every insert, so building from n unsorted or
reverse-sorted items was O(n^2). OrderedSet.ToComparerSet goes through that
constructor, which made IsSubsetOf, IsProperSubsetOf, IsProperSupersetOf,
SetEquals, IntersectWith and SymmetricExceptWith quadratic in the size of the
argument: over a minute for a 1,000,000-element descending array.

The constructors now copy the input and run a stable merge sort (new internal
StableSort) with the container's comparer:
- OrderedSet drops adjacent equal elements, keeping the first, as Add does
- OrderedCollection keeps equal elements in their original order, as Add does
- OrderedMap throws the same ArgumentException Add throws when two keys
  compare equal

OrderedSet set operations also reuse an OrderedSet argument that has the same
comparer instead of copying it, IntersectWith compacts with a single RemoveAll,
and SymmetricExceptWith merges the two sorted lists in one linear pass.

Fixes #82

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6bDUGMFsAQews3TnA2jXr
@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 1d6250f into main Oct 8, 2026
14 checks passed
@matt-edmondson
matt-edmondson deleted the claude/issue-82-bulk-sort branch October 8, 2026 04:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants