//****************************** // Written by Peter Golde // Copyright (c) 2004-2005, Wintellect // // Use and restribution of this code is subject to the license agreement // contained in the file "License.txt" accompanying this file. //****************************** using System; using System.Collections.Generic; using System.Collections; // CONSIDER: RemoveIdentical to remove an identical item only. Can this be done with current RedBlack tree implementation? How // CONSIDER: useful is it? namespace Wintellect.PowerCollections { /// /// OrderedBag<T> is a collection that contains items of type T. /// The item are maintained in a sorted order. Unlike a OrderedSet, duplicate items (items that /// compare equal to each other) are allows in an OrderedBag. /// /// ///

The items are compared in one of three ways. If T implements IComparable<TKey> or IComparable, /// then the CompareTo method of that interface will be used to compare items. Alternatively, a comparison /// function can be passed in either as a delegate, or as an instance of IComparer<TKey>.

///

OrderedBag is implemented as a balanced binary tree. Inserting, deleting, and looking up an /// an element all are done in log(N) + M time, where N is the number of keys in the tree, and M is the current number /// of copies of the element being handled.

///

is similar, but uses hashing instead of comparison, and does not maintain /// the keys in sorted order.

///
/// [Serializable] public class OrderedBag : CollectionBase, ICloneable { // The comparer used to compare items. private IComparer comparer; // The red-black tree that actually does the work of storing the items. private RedBlackTree tree; #region Constructors /// /// Creates a new OrderedBag. The T must implement IComparable<T> /// or IComparable. /// The CompareTo method of this interface will be used to compare items in this bag. /// /// /// Items that are null are permitted, and will be sorted before all other items. /// /// T does not implement IComparable<TKey>. public OrderedBag(): this(Comparers.DefaultComparer()) { } /// /// Creates a new OrderedBag. The passed delegate will be used to compare items in this bag. /// /// A delegate to a method that will be used to compare items. public OrderedBag(Comparison comparison) : this(Comparers.ComparerFromComparison(comparison)) { } /// /// Creates a new OrderedBag. The Compare method of the passed comparison object /// will be used to compare items in this bag. /// /// /// The GetHashCode and Equals methods of the provided IComparer<T> will never /// be called, and need not be implemented. /// /// An instance of IComparer<T> that will be used to compare items. public OrderedBag(IComparer comparer) { if (comparer == null) throw new ArgumentNullException("comparer"); this.comparer = comparer; tree = new RedBlackTree(comparer); } /// /// Creates a new OrderedBag. The T must implement IComparable<T> /// or IComparable. /// The CompareTo method of this interface will be used to compare items in this bag. The bag is /// initialized with all the items in the given collection. /// /// /// Items that are null are permitted, and will be sorted before all other items. /// /// A collection with items to be placed into the OrderedBag. /// T does not implement IComparable<TKey>. public OrderedBag(IEnumerable collection): this(collection, Comparers.DefaultComparer()) { } /// /// Creates a new OrderedBag. The passed delegate will be used to compare items in this bag. /// The bag is initialized with all the items in the given collection. /// /// A collection with items to be placed into the OrderedBag. /// A delegate to a method that will be used to compare items. public OrderedBag(IEnumerable collection, Comparison comparison): this(collection, Comparers.ComparerFromComparison(comparison)) { } /// /// Creates a new OrderedBag. The Compare method of the passed comparison object /// will be used to compare items in this bag. The bag is /// initialized with all the items in the given collection. /// /// /// The GetHashCode and Equals methods of the provided IComparer<T> will never /// be called, and need not be implemented. /// /// A collection with items to be placed into the OrderedBag. /// An instance of IComparer<T> that will be used to compare items. public OrderedBag(IEnumerable collection, IComparer comparer): this(comparer) { AddMany(collection); } /// /// Creates a new OrderedBag given a comparer and a tree that contains the data. Used /// internally for Clone. /// /// Comparer for the bag. /// Data for the bag. private OrderedBag(IComparer comparer, RedBlackTree tree) { this.comparer = comparer; this.tree = tree; } #endregion Constructors #region Cloning /// /// Makes a shallow clone of this bag; i.e., if items of the /// bag are reference types, then they are not cloned. If T is a value type, /// then each element is copied as if by simple assignment. /// /// Cloning the bag takes time O(N), where N is the number of items in the bag. /// The cloned bag. object ICloneable.Clone() { return this.Clone(); } /// /// Makes a shallow clone of this bag; i.e., if items of the /// bag are reference types, then they are not cloned. If T is a value type, /// then each element is copied as if by simple assignment. /// /// Cloning the bag takes time O(N), where N is the number of items in the bag. /// The cloned bag. public OrderedBag Clone() { OrderedBag newBag = new OrderedBag(comparer, tree.Clone()); return newBag; } /// /// Makes a deep clone of this bag. A new bag is created with a clone of /// each element of this bag, by calling ICloneable.Clone on each element. If T is /// a value type, then each element is copied as if by simple assignment. /// /// If T is a reference type, it must implement /// ICloneable. Otherwise, an InvalidOperationException is thrown. /// Cloning the bag takes time O(N log N), where N is the number of items in the bag. /// The cloned bag. /// T is a reference type that does not implement ICloneable. public OrderedBag CloneContents() { bool itemIsValueType; if (!Util.IsCloneableType(typeof(T), out itemIsValueType)) throw new InvalidOperationException(string.Format(Strings.TypeNotCloneable, typeof(T).FullName)); OrderedBag clone = new OrderedBag(comparer); // Clone each item, and add it to the new ordered bag. foreach (T item in this) { T itemClone; if (itemIsValueType) itemClone = item; else { if (item == null) itemClone = default(T); // Really null, because we know T is a reference type else itemClone = (T)(((ICloneable)item).Clone()); } clone.Add(itemClone); } return clone; } #endregion Cloning #region Basic collection containment /// /// Returns the IComparer<T> used to compare items in this bag. /// /// If the bag was created using a comparer, that comparer is returned. If the bag was /// created using a comparison delegate, then a comparer equivalent to that delegate /// is returned. Otherwise /// the default comparer for T (Comparer<T>.Default) is returned. public IComparer Comparer { get { return this.comparer; } } /// /// Returns the number of items in the bag. /// /// The size of the bag is returned in constant time. /// The number of items in the bag. public sealed override int Count { get { return tree.ElementCount; } } /// /// Returns the number of copies of in the bag. More precisely, returns /// the number of items in the bag that compare equal to . /// /// NumberOfCopies() takes time O(log N + M), where N is the total number of items in the /// bag, and M is the number of copies of in the bag. /// The item to search for in the bag. /// The number of items in the bag that compare equal to . public int NumberOfCopies(T item) { return tree.CountRange(tree.EqualRangeTester(item)); } /// /// Returns an enumerator that enumerates all the items in the bag. /// The items are enumerated in sorted order. /// /// ///

Typically, this method is not called directly. Instead the "foreach" statement is used /// to enumerate the items, which uses this method implicitly.

///

If an item is added to or deleted from the bag while it is being enumerated, then /// the enumeration will end with an InvalidOperationException.

///

Enumeration all the items in the bag takes time O(N), where N is the number /// of items in the bag.

///
/// An enumerator for enumerating all the items in the OrderedBag. public sealed override IEnumerator GetEnumerator() { return tree.GetEnumerator(); } /// /// Determines if this bag contains an item equal to . The bag /// is not changed. /// /// Searching the bag for an item takes time O(log N), where N is the number of items in the bag. /// The item to search for. /// True if the bag contains . False if the bag does not contain . public sealed override bool Contains(T item) { T dummy; return tree.Find(item, false, false, out dummy); } /// /// Enumerates all of the items in this bag that are equal to , according to the /// comparison mechanism that was used when the bag was created. The bag /// is not changed. /// If the bag does contain an item equal to , then the enumeration contains /// no items. /// /// Enumeration the items in the bag equal to takes time O(log N + M), where N /// is the total number of items in the bag, and M is the number of items equal to . /// The item to search for. /// An IEnumerable<T> that enumerates all the items in the bag equal to . public IEnumerable GetEqualItems(T item) { return tree.EnumerateRange(tree.EqualRangeTester(item)); } /// /// Enumerates all the items in the bag, but enumerates equal items /// just once, even if they occur multiple times in the bag. /// /// If the bag is changed while items are being enumerated, the /// enumeration will terminate with an InvalidOperationException. /// An IEnumerable<T> that enumerates the unique items. public IEnumerable DistinctItems() { T previous = default(T); bool atBeginning = true; // Enumerate the items, but only yield ones not equal to the previous one. foreach (T item in this) { if (atBeginning || comparer.Compare(item, previous) != 0) yield return item; previous = item; atBeginning = false; } } #endregion #region Index by sorted order /// /// Get the item by its index in the sorted order. The smallest item has index 0, /// the next smallest item has index 1, and the largest item has index Count-1. /// /// The indexer takes time O(log N), which N is the number of items in /// the set. /// The index to get the item by. /// The item at the given index. /// is /// less than zero or greater than or equal to Count. public T this[int index] { get { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException("index"); return tree.GetItemByIndex(index); } } /// /// Get the index of the given item in the sorted order. The smallest item has index 0, /// the next smallest item has index 1, and the largest item has index Count-1. If multiple /// equal items exist, the largest index of the equal items is returned. /// /// Finding the index takes time O(log N), which N is the number of items in /// the set. /// The item to get the index of. /// The index of the last item in the sorted bag equal to , or -1 if the item is not present /// in the set. public int LastIndexOf(T item) { return tree.FindIndex(item, false); } /// /// Get the index of the given item in the sorted order. The smallest item has index 0, /// the next smallest item has index 1, and the largest item has index Count-1. If multiple /// equal items exist, the smallest index of the equal items is returned. /// /// Finding the index takes time O(log N), which N is the number of items in /// the set. /// The item to get the index of. /// The index of the first item in the sorted bag equal to , or -1 if the item is not present /// in the set. public int IndexOf(T item) { return tree.FindIndex(item, true); } #endregion #region Adding elements /// /// Adds a new item to the bag. Since bags can contain duplicate items, the item /// is added even if the bag already contains an item equal to . In /// this case, the new item is placed after all equal items already present in the bag. /// /// /// Adding an item takes time O(log N), where N is the number of items in the bag. /// The item to add to the bag. public sealed override void Add(T item) { T dummy; tree.Insert(item, DuplicatePolicy.InsertLast, out dummy); } /// /// Adds all the items in to the bag. /// /// /// Adding the collection takes time O(M log N), where N is the number of items in the bag, and M is the /// number of items in . /// A collection of items to add to the bag. /// is null. public void AddMany(IEnumerable collection) { if (collection == null) throw new ArgumentNullException("collection"); // If we're adding ourselves, we need to copy to a separate array to avoid modification // during enumeration. if (this == collection) collection = this.ToArray(); foreach (T item in collection) Add(item); } #endregion Adding elements #region Removing elements /// /// Searches the bag for one item equal to , and if found, /// removes it from the bag. If not found, the bag is unchanged. If more than one item /// equal to , the item that was last inserted is removed. /// /// /// Equality between items is determined by the comparison instance or delegate used /// to create the bag. /// Removing an item from the bag takes time O(log N), where N is the number of items in the bag. /// The item to remove. /// True if was found and removed. False if was not in the bag. public sealed override bool Remove(T item) { T dummy; return tree.Delete(item, false, out dummy); } /// /// Searches the bag for all items equal to , and /// removes all of them from the bag. If not found, the bag is unchanged. /// /// /// Equality between items is determined by the comparison instance or delegate used /// to create the bag. /// RemoveAllCopies() takes time O(M log N), where N is the total number of items in the bag, and M is /// the number of items equal to . /// The item to remove. /// The number of copies of that were found and removed. public int RemoveAllCopies(T item) { return tree.DeleteRange(tree.EqualRangeTester(item)); } /// /// Removes all the items in from the bag. Items not /// present in the bag are ignored. /// /// /// Equality between items is determined by the comparison instance or delegate used /// to create the bag. /// Removing the collection takes time O(M log N), where N is the number of items in the bag, and M is the /// number of items in . /// A collection of items to remove from the bag. /// The number of items removed from the bag. /// is null. public int RemoveMany(IEnumerable collection) { if (collection == null) throw new ArgumentNullException("collection"); int count = 0; if (collection == this) { count = Count; Clear(); // special case, otherwise we will throw. } else { foreach (T item in collection) { if (Remove(item)) ++count; } } return count; } /// /// Removes all items from the bag. /// /// Clearing the bag takes a constant amount of time, regardless of the number of items in it. public sealed override void Clear() { tree.StopEnumerations(); // Invalidate any enumerations. // The simplest and fastest way is simply to throw away the old tree and create a new one. tree = new RedBlackTree(comparer); } #endregion Removing elements #region First/last items /// /// If the collection is empty, throw an invalid operation exception. /// /// The bag is empty. private void CheckEmpty() { if (Count == 0) throw new InvalidOperationException(Strings.CollectionIsEmpty); } /// /// Returns the first item in the bag: the item /// that would appear first if the bag was enumerated. This is also /// the smallest item in the bag. /// /// GetFirst() takes time O(log N), where N is the number of items in the bag. /// The first item in the bag. If more than one item /// is smallest, the first one added is returned. /// The bag is empty. public T GetFirst() { T item; CheckEmpty(); tree.FirstItemInRange(tree.EntireRangeTester, out item); return item; } /// /// Returns the last item in the bag: the item /// that would appear last if the bag was enumerated. This is also the largest /// item in the bag. /// /// GetLast() takes time O(log N), where N is the number of items in the bag. /// The last item in the bag. If more than one item /// is largest, the last one added is returned. /// The bag is empty. public T GetLast() { T item; CheckEmpty(); tree.LastItemInRange(tree.EntireRangeTester, out item); return item; } /// /// Removes the first item in the bag. This is also the smallest /// item in the bag. /// /// RemoveFirst() takes time O(log N), where N is the number of items in the bag. /// The item that was removed, which was the smallest item in the bag. /// The bag is empty. public T RemoveFirst() { CheckEmpty(); T item; tree.DeleteItemFromRange(tree.EntireRangeTester, true, out item); return item; } /// /// Removes the last item in the bag. This is also the largest item in the bag. /// /// RemoveLast() takes time O(log N), where N is the number of items in the bag. /// The item that was removed, which was the largest item in the bag. /// The bag is empty. public T RemoveLast() { CheckEmpty(); T item; tree.DeleteItemFromRange(tree.EntireRangeTester, false, out item); return item; } #endregion #region Set operations /// /// Check that this bag and another bag were created with the same comparison /// mechanism. Throws exception if not compatible. /// /// Other bag to check comparision mechanism. /// If otherBag and this bag don't use the same method for comparing items. /// is null. private void CheckConsistentComparison(OrderedBag otherBag) { if (otherBag == null) throw new ArgumentNullException("otherBag"); if (!object.Equals(comparer, otherBag.comparer)) throw new InvalidOperationException(Strings.InconsistentComparisons); } /// /// Determines if this bag is a superset of another bag. Neither bag is modified. /// This bag is a superset of if every element in /// is also in this bag, at least the same number of /// times. /// /// IsSupersetOf is computed in time O(M log N), where M is the size of the /// , and N is the size of the this set. /// OrderedBag to compare to. /// True if this is a superset of . /// This bag and don't use the same method for comparing items. /// is null. public bool IsSupersetOf(OrderedBag otherBag) { CheckConsistentComparison(otherBag); if (otherBag.Count > this.Count) return false; // Can't be a superset of a bigger bag // Check each item in the other bag to make sure it is in this bag. foreach (T item in otherBag.DistinctItems()) { if (this.NumberOfCopies(item) < otherBag.NumberOfCopies(item)) return false; } return true; } /// /// Determines if this bag is a proper superset of another bag. Neither bag is modified. /// This bag is a proper superset of if every element in /// is also in this bag, at least the same number of /// times. Additional, this bag must have strictly more items than . /// /// IsProperSupersetOf is computed in time O(M log N), where M is the number of unique items in /// . /// OrderedBag to compare to. /// True if this is a proper superset of . /// This bag and don't use the same method for comparing items. /// is null. public bool IsProperSupersetOf(OrderedBag otherBag) { CheckConsistentComparison(otherBag); if (otherBag.Count >= this.Count) return false; // Can't be a proper superset of a bigger or equal set return IsSupersetOf(otherBag); } /// /// Determines if this bag is a subset of another bag. Neither bag is modified. /// This bag is a subset of if every element in this bag /// is also in , at least the same number of /// times. /// /// IsSubsetOf is computed in time O(N log M), where M is the size of the /// , and N is the size of the this bag. /// OrderedBag to compare to. /// True if this is a subset of . /// This bag and don't use the same method for comparing items. /// is null. public bool IsSubsetOf(OrderedBag otherBag) { return otherBag.IsSupersetOf(this); } /// /// Determines if this bag is a proper subset of another bag. Neither bag is modified. /// This bag is a subset of if every element in this bag /// is also in , at least the same number of /// times. Additional, this bag must have strictly fewer items than . /// /// IsSubsetOf is computed in time O(N log M), where M is the size of the /// , and N is the size of the this bag. /// OrderedBag to compare to. /// True if this is a proper subset of . /// This bag and don't use the same method for comparing items. /// is null. public bool IsProperSubsetOf(OrderedBag otherBag) { return otherBag.IsProperSupersetOf(this); } /// /// Determines if this bag is disjoint from another bag. Two bags are disjoint /// if no item from one set is equal to any item in the other bag. /// /// /// The answer is computed in time O(N), where N is the size of the smaller set. /// /// Bag to check disjointness with. /// True if the two bags are disjoint, false otherwise. /// This bag and don't use the same method for comparing items. public bool IsDisjointFrom(OrderedBag otherBag) { CheckConsistentComparison(otherBag); OrderedBag smaller, larger; if (otherBag.Count > this.Count) { smaller = this; larger = otherBag; } else { smaller = otherBag; larger = this; } foreach (T item in smaller) { if (larger.Contains(item)) return false; } return true; } /// /// Determines if this bag is equal to another bag. This bag is equal to /// if they contain the same items, each the /// same number of times. /// /// IsEqualTo is computed in time O(N), where N is the number of items in /// this bag. /// OrderedBag to compare to /// True if this bag is equal to , false otherwise. /// This bag and don't use the same method for comparing items. public bool IsEqualTo(OrderedBag otherBag) { CheckConsistentComparison(otherBag); // Must be the same size. if (otherBag.Count != this.Count) return false; // Since both bags are ordered, we can simply compare items in order. using (IEnumerator enum1 = this.GetEnumerator(), enum2 = otherBag.GetEnumerator()) { bool continue1, continue2; for (; ; ) { continue1 = enum1.MoveNext(); continue2 = enum2.MoveNext(); if (!continue1 || !continue2) break; if (comparer.Compare(enum1.Current, enum2.Current) != 0) return false; // the two items are not equal. } // If both continue1 and continue2 are false, we reached the end of both sequences at the same // time and found success. If one is true and one is false, the sequences were of difference lengths -- failure. return (continue1 == continue2); } } /// /// Computes the union of this bag with another bag. The union of two bags /// is all items from both of the bags. If an item appears X times in one bag, /// and Y times in the other bag, the union contains the item Maximum(X,Y) times. This bag receives /// the union of the two bags, the other bag is unchanged. /// /// /// The union of two bags is computed in time O(M + N log M), where M is the size of the /// larger bag, and N is the size of the smaller bag. /// /// Bag to union with. /// This bag and don't use the same method for comparing items. /// is null. public void UnionWith(OrderedBag otherBag) { CheckConsistentComparison(otherBag); T previous = default(T); bool atBeginning = true; int copiesInThis = 0, copiesInOther = 0; // Enumerate each of the items in the other bag. Add items that need to be // added to this bag. // CONSIDER: may be able to improve this algorithm if otherBag is larger than this bag. foreach (T item in otherBag) { if (atBeginning || comparer.Compare(item, previous) != 0) { copiesInThis = this.NumberOfCopies(item); copiesInOther = 1; } else { ++copiesInOther; } if (copiesInOther > copiesInThis) this.Add(item); previous = item; atBeginning = false; } } /// /// Computes the union of this bag with another bag. The union of two bags /// is all items from both of the bags. If an item appears X times in one bag, /// and Y times in the other bag, the union contains the item Maximum(X,Y) times. A new bag is /// created with the union of the bags and is returned. This bag and the other bag /// are unchanged. /// /// /// The union of two bags is computed in time O(M + N log M), where M is the size of the /// larger bag, and N is the size of the smaller bag. /// /// Bag to union with. /// The union of the two bags. /// This bag and don't use the same method for comparing items. /// is null. public OrderedBag Union(OrderedBag otherBag) { CheckConsistentComparison(otherBag); OrderedBag smaller, larger, result; if (otherBag.Count > this.Count) { smaller = this; larger = otherBag; } else { smaller = otherBag; larger = this; } result = larger.Clone(); result.UnionWith(smaller); return result; } /// /// Computes the sum of this bag with another bag. The sum of two bags /// is all items from both of the bags. If an item appears X times in one bag, /// and Y times in the other bag, the sum contains the item (X+Y) times. This bag receives /// the sum of the two bags, the other bag is unchanged. /// /// /// The sum of two bags is computed in time O(M + N log M), where M is the size of the /// larger bag, and N is the size of the smaller bag. /// /// Bag to sum with. /// This bag and don't use the same method for comparing items. /// is null. public void SumWith(OrderedBag otherBag) { CheckConsistentComparison(otherBag); AddMany(otherBag); // CONSIDER: if otherBag is much larger, maybe better to clone it, // add all of the current into it, and replace. } /// /// Computes the sum of this bag with another bag. he sum of two bags /// is all items from both of the bags. If an item appears X times in one bag, /// and Y times in the other bag, the sum contains the item (X+Y) times. A new bag is /// created with the sum of the bags and is returned. This bag and the other bag /// are unchanged. /// /// /// The sum of two bags is computed in time O(M + N log M), where M is the size of the /// larger bag, and N is the size of the smaller bag. /// /// Bag to sum with. /// The sum of the two bags. /// This bag and don't use the same method for comparing items. /// is null. public OrderedBag Sum(OrderedBag otherBag) { CheckConsistentComparison(otherBag); OrderedBag smaller, larger, result; if (otherBag.Count > this.Count) { smaller = this; larger = otherBag; } else { smaller = otherBag; larger = this; } result = larger.Clone(); result.AddMany(smaller); return result; } /// /// Computes the intersection of this bag with another bag. The intersection of two bags /// is all items that appear in both of the bags. If an item appears X times in one bag, /// and Y times in the other bag, the sum contains the item Minimum(X,Y) times. This bag receives /// the intersection of the two bags, the other bag is unchanged. /// /// /// When equal items appear in both bags, the intersection will include an arbitrary choice of one of the /// two equal items. /// The intersection of two bags is computed in time O(N log M), where M is the size of the /// larger bag, and N is the size of the smaller bag. /// /// Bag to intersection with. /// This bag and don't use the same method for comparing items. /// is null. public void IntersectionWith(OrderedBag otherBag) { CheckConsistentComparison(otherBag); tree.StopEnumerations(); OrderedBag smaller, larger; if (otherBag.Count > this.Count) { smaller = this; larger = otherBag; } else { smaller = otherBag; larger = this; } T dummy; RedBlackTree newTree = new RedBlackTree(comparer); T previous = default(T); bool atBeginning = true; int copiesInSmaller = 0, copiesInLarger = 0; // Enumerate each of the items in the smaller bag. Add items that need to be // added to the intersection. foreach (T item in smaller) { if (atBeginning || comparer.Compare(item, previous) != 0) { copiesInLarger = larger.NumberOfCopies(item); copiesInSmaller = 1; } else { ++copiesInSmaller; } if (copiesInSmaller <= copiesInLarger) newTree.Insert(item, DuplicatePolicy.InsertLast, out dummy); previous = item; atBeginning = false; } tree = newTree; } /// /// Computes the intersection of this bag with another bag. The intersection of two bags /// is all items that appear in both of the bags. If an item appears X times in one bag, /// and Y times in the other bag, the sum contains the item Minimum(X,Y) times. A new bag is /// created with the intersection of the bags and is returned. This bag and the other bag /// are unchanged. /// /// /// When equal items appear in both bags, the intersection will include an arbitrary choice of one of the /// two equal items. /// The intersection of two bags is computed in time O(N log M), where M is the size of the /// larger bag, and N is the size of the smaller bag. /// /// Bag to intersection with. /// The intersection of the two bags. /// This bag and don't use the same method for comparing items. /// is null. public OrderedBag Intersection(OrderedBag otherBag) { CheckConsistentComparison(otherBag); OrderedBag smaller, larger, result; if (otherBag.Count > this.Count) { smaller = this; larger = otherBag; } else { smaller = otherBag; larger = this; } T previous = default(T); bool atBeginning = true; int copiesInSmaller = 0, copiesInLarger = 0; // Enumerate each of the items in the smaller bag. Add items that need to be // added to the intersection. result = new OrderedBag(comparer); foreach (T item in smaller) { if (atBeginning || comparer.Compare(item, previous) != 0) { copiesInLarger = larger.NumberOfCopies(item); copiesInSmaller = 1; } else { ++copiesInSmaller; } if (copiesInSmaller <= copiesInLarger) result.Add(item); previous = item; atBeginning = false; } return result; } /// /// Computes the difference of this bag with another bag. The difference of these two bags /// is all items that appear in this bag, but not in . If an item appears X times in this bag, /// and Y times in the other bag, the difference contains the item X - Y times (zero times if Y >= X). This bag receives /// the difference of the two bags; the other bag is unchanged. /// /// /// The difference of two bags is computed in time O(M + N log M), where M is the size of the /// larger bag, and N is the size of the smaller bag. /// /// Bag to difference with. /// This bag and don't use the same method for comparing items. /// is null. public void DifferenceWith(OrderedBag otherBag) { // Difference with myself is nothing. This check is needed because the // main algorithm doesn't work correctly otherwise. if (this == otherBag) Clear(); CheckConsistentComparison(otherBag); T previous = default(T); bool atBeginning = true; int copiesInThis = 0, copiesInOther = 0; // Enumerate each of the items in the other bag. Remove items that need to be // removed from this bag. // CONSIDER: should be able to improve this algorithm if otherBag is larger than this bag. foreach (T item in otherBag) { if (atBeginning || comparer.Compare(item, previous) != 0) { copiesInThis = this.NumberOfCopies(item); copiesInOther = 1; } else { ++copiesInOther; } if (copiesInOther <= copiesInThis) this.Remove(item); previous = item; atBeginning = false; } } /// /// Computes the difference of this bag with another bag. The difference of these two bags /// is all items that appear in this bag, but not in . If an item appears X times in this bag, /// and Y times in the other bag, the difference contains the item X - Y times (zero times if Y >= X). A new bag is /// created with the difference of the bags and is returned. This bag and the other bag /// are unchanged. /// /// /// The difference of two bags is computed in time O(M + N log M), where M is the size of the /// larger bag, and N is the size of the smaller bag. /// /// Bag to difference with. /// The difference of the two bags. /// This bag and don't use the same method for comparing items. /// is null. public OrderedBag Difference(OrderedBag otherBag) { CheckConsistentComparison(otherBag); OrderedBag result = this.Clone(); result.DifferenceWith(otherBag); return result; } /// /// Computes the symmetric difference of this bag with another bag. The symmetric difference of two bags /// is all items that appear in either of the bags, but not both. If an item appears X times in one bag, /// and Y times in the other bag, the symmetric difference contains the item AbsoluteValue(X - Y times). This bag receives /// the symmetric difference of the two bags; the other bag is unchanged. /// /// /// The symmetric difference of two bags is computed in time O(M + N), where M is the size of the /// larger bag, and N is the size of the smaller bag. /// /// Bag to symmetric difference with. /// This bag and don't use the same method for comparing items. /// is null. public void SymmetricDifferenceWith(OrderedBag otherBag) { tree = SymmetricDifference(otherBag).tree; } /// /// Computes the symmetric difference of this bag with another bag. The symmetric difference of two bags /// is all items that appear in either of the bags, but not both. If an item appears X times in one bag, /// and Y times in the other bag, the symmetric difference contains the item AbsoluteValue(X - Y times). A new bag is /// created with the symmetric difference of the bags and is returned. This bag and the other bag /// are unchanged. /// /// /// The symmetric difference of two bags is computed in time O(M + N), where M is the size of the /// larger bag, and N is the size of the smaller bag. /// /// Bag to symmetric difference with. /// The symmetric difference of the two bags. /// This bag and don't use the same method for comparing items. /// is null. public OrderedBag SymmetricDifference(OrderedBag otherBag) { CheckConsistentComparison(otherBag); OrderedBag result = new OrderedBag(comparer); IEnumerator enum1 = this.GetEnumerator(), enum2 = otherBag.GetEnumerator(); bool valid1 = enum1.MoveNext(); bool valid2 = enum2.MoveNext(); int compare; for (;;) { // Which item is smaller? The end (!valid) is considered larger than any item. if (! valid1) { if (! valid2) break; compare = 1; } else if (! valid2) compare = -1; else compare = comparer.Compare(enum1.Current, enum2.Current); // If equal, move through both bags without adding. Otherwise, add the smaller item and advance // just through that bag. if (compare == 0) { valid1 = enum1.MoveNext(); valid2 = enum2.MoveNext(); } else if (compare < 0) { result.Add(enum1.Current); valid1 = enum1.MoveNext(); } else { // compare > 0 result.Add(enum2.Current); valid2 = enum2.MoveNext(); } } return result; } #endregion Set operations #region Read-only list view /// /// Get a read-only list view of the items in this ordered bag. The /// items in the list are in sorted order, with the smallest item /// at index 0. This view does not copy any data, and reflects any /// changes to the underlying OrderedBag. /// /// A read-only IList<T> view onto this OrderedBag. public IList AsList() { return new ListView(this, tree.EntireRangeTester, true, false); } /// /// The nested class that provides a read-only list view /// of all or part of the collection. /// [Serializable] private class ListView : ReadOnlyListBase { private OrderedBag myBag; private RedBlackTree.RangeTester rangeTester; // range tester for the range being used. private bool entireTree; // is the view the whole tree? private bool reversed; // is the view reversed? /// /// Create a new list view wrapped the given set. /// /// The ordered bag to wrap. /// Range tester that defines the range being used. /// If true, then rangeTester defines the entire tree. Used to optimize some operations. /// Is the view enuemerated in reverse order? public ListView(OrderedBag myBag, RedBlackTree.RangeTester rangeTester, bool entireTree, bool reversed) { this.myBag = myBag; this.rangeTester = rangeTester; this.entireTree = entireTree; this.reversed = reversed; } public sealed override int Count { get { if (entireTree) return myBag.Count; else { // Note: we can't cache the result of this call because the underlying // set can change, which would make the cached value incorrect. return myBag.tree.CountRange(rangeTester); } } } public sealed override T this[int index] { get { if (entireTree) { if (reversed) return myBag[myBag.Count - 1 - index]; else return myBag[index]; } else { T dummy; int firstIndex = myBag.tree.FirstItemInRange(rangeTester, out dummy); int lastIndex = myBag.tree.LastItemInRange(rangeTester, out dummy); if (firstIndex < 0 || lastIndex < 0 || index < 0 || index >= (lastIndex - firstIndex + 1)) throw new ArgumentOutOfRangeException("index"); if (reversed) return myBag[lastIndex - index]; else return myBag[firstIndex + index]; } } } public sealed override int IndexOf(T item) { if (entireTree) { if (reversed) return myBag.Count - 1 - myBag.LastIndexOf(item); else return myBag.IndexOf(item); } else { T dummy; if (rangeTester(item) != 0) return -1; if (reversed) { int indexInSet = myBag.tree.FindIndex(item, false); if (indexInSet < 0) return -1; int indexOfEnd = myBag.tree.LastItemInRange(rangeTester, out dummy); return indexOfEnd - indexInSet; } else { int indexInSet = myBag.tree.FindIndex(item, true); if (indexInSet < 0) return -1; int indexOfStart = myBag.tree.FirstItemInRange(rangeTester, out dummy); return indexInSet - indexOfStart; } } } } #endregion Read-only list view #region Sub-views /// /// Returns a View collection that can be used for enumerating the items in the bag in /// reversed order. /// /// ///

Typically, this method is used in conjunction with a foreach statement. For example: /// /// foreach(T item in bag.Reversed()) { /// // process item /// } ///

///

If an item is added to or deleted from the bag while the View is being enumerated, then /// the enumeration will end with an InvalidOperationException.

///

Calling Reverse does not copy the data in the tree, and the operation takes constant time.

///
/// An OrderedBag.View of items in reverse order. public View Reversed() // A reversed view that can be enumerated { return new View(this, tree.EntireRangeTester, true, true); } /// /// Returns a View collection that can be used for enumerating a range of the items in the bag. /// Only items that are greater than and /// less than are included. The items are enumerated in sorted order. /// Items equal to the end points of the range can be included or excluded depending on the /// and parameters. /// /// ///

If is greater than or equal to , the returned collection is empty.

///

Typically, this method is used in conjunction with a foreach statement. For example: /// /// foreach(T item in bag.Range(from, true, to, false)) { /// // process item /// } ///

///

If an item is added to or deleted from the bag while the View is being enumerated, then /// the enumeration will end with an InvalidOperationException.

///

Calling Range does not copy the data in the tree, and the operation takes constant time.

///
/// The lower bound of the range. /// If true, the lower bound is inclusive--items equal to the lower bound will /// be included in the range. If false, the lower bound is exclusive--items equal to the lower bound will not /// be included in the range. /// The upper bound of the range. /// If true, the upper bound is inclusive--items equal to the upper bound will /// be included in the range. If false, the upper bound is exclusive--items equal to the upper bound will not /// be included in the range. /// An OrderedBag.View of items in the given range. public View Range(T from, bool fromInclusive, T to, bool toInclusive) // A partial view that can be enumerated { return new View(this, tree.DoubleBoundedRangeTester(from, fromInclusive, to, toInclusive), false, false); } /// /// Returns a View collection that can be used for enumerating a range of the items in the bag. /// Only items that are greater than (and optionally, equal to) are included. /// The items are enumerated in sorted order. Items equal to can be included /// or excluded depending on the parameter. /// /// ///

Typically, this method is used in conjunction with a foreach statement. For example: /// /// foreach(T item in bag.RangeFrom(from, true)) { /// // process item /// } ///

///

If an item is added to or deleted from the bag while the View is being enumerated, then /// the enumeration will end with an InvalidOperationException.

///

Calling RangeFrom does not copy the data in the tree, and the operation takes constant time.

///
/// The lower bound of the range. /// If true, the lower bound is inclusive--items equal to the lower bound will /// be included in the range. If false, the lower bound is exclusive--items equal to the lower bound will not /// be included in the range. /// An OrderedBag.View of items in the given range. public View RangeFrom(T from, bool fromInclusive) // A partial view that can be enumerated { return new View(this, tree.LowerBoundedRangeTester(from, fromInclusive), false, false); } /// /// Returns a View collection that can be used for enumerating a range of the items in the bag. /// Only items that are less than (and optionally, equal to) are included. /// The items are enumerated in sorted order. Items equal to can be included /// or excluded depending on the parameter. /// /// ///

Typically, this method is used in conjunction with a foreach statement. For example: /// /// foreach(T item in bag.RangeTo(to, false)) { /// // process item /// } ///

///

If an item is added to or deleted from the bag while the View is being enumerated, then /// the enumeration will end with an InvalidOperationException.

///

Calling RangeTo does not copy the data in the tree, and the operation takes constant time.

///
/// The upper bound of the range. /// If true, the upper bound is inclusive--items equal to the upper bound will /// be included in the range. If false, the upper bound is exclusive--items equal to the upper bound will not /// be included in the range. /// An OrderedBag.View of items in the given range. public View RangeTo(T to, bool toInclusive) // A partial view that can be enumerated { return new View(this, tree.UpperBoundedRangeTester(to, toInclusive), false, false); } #endregion #region View nested class /// /// The OrderedBag<T>.View class is used to look at a subset of the items /// inside an ordered bag. It is returned from the Range, RangeTo, RangeFrom, and Reversed methods. /// /// ///

Views are dynamic. If the underlying bag changes, the view changes in sync. If a change is made /// to the view, the underlying bag changes accordingly.

///

Typically, this class is used in conjunction with a foreach statement to enumerate the items /// in a subset of the OrderedBag. For example:

/// /// foreach(T item in bag.Range(from, to)) { /// // process item /// } /// ///
[Serializable] public class View : CollectionBase { private OrderedBag myBag; private RedBlackTree.RangeTester rangeTester; // range tester for the range being used. private bool entireTree; // is the view the whole tree? private bool reversed; // is the view reversed? /// /// Initialize the view. /// /// OrderedBag being viewed /// Range tester that defines the range being used. /// If true, then rangeTester defines the entire tree. /// Is the view enuemerated in reverse order? internal View(OrderedBag myBag, RedBlackTree.RangeTester rangeTester, bool entireTree, bool reversed) { this.myBag = myBag; this.rangeTester = rangeTester; this.entireTree = entireTree; this.reversed = reversed; } /// /// Determine if the given item lies within the bounds of this view. /// /// Item to test. /// True if the item is within the bounds of this view. private bool ItemInView(T item) { return rangeTester(item) == 0; } /// /// Enumerate all the items in this view. /// /// An IEnumerator<T> with the items in this view. public sealed override IEnumerator GetEnumerator() { if (reversed) return myBag.tree.EnumerateRangeReversed(rangeTester).GetEnumerator(); else return myBag.tree.EnumerateRange(rangeTester).GetEnumerator(); } /// /// Number of items in this view. /// /// Number of items that lie within the bounds the view. public sealed override int Count { get { if (entireTree) return myBag.Count; else { // Note: we can't cache the result of this call because the underlying // set can change, which would make the cached value incorrect. return myBag.tree.CountRange(rangeTester); } } } /// /// Removes all the items within this view from the underlying bag. /// /// The following removes all the items that start with "A" from an OrderedBag. /// /// bag.Range("A", "B").Clear(); /// /// public sealed override void Clear() { if (entireTree) { myBag.Clear(); } else { myBag.tree.DeleteRange(rangeTester); } } /// /// Adds a new item to the bag underlying this View. If the bag already contains an item equal to /// , that item is replaces with . If /// is outside the range of this view, an InvalidOperationException /// is thrown. /// /// /// Equality between items is determined by the comparison instance or delegate used /// to create the bag. /// Adding an item takes time O(log N), where N is the number of items in the bag. /// The item to add. /// True if the bag already contained an item equal to (which was replaced), false /// otherwise. public sealed override void Add(T item) { if (!ItemInView(item)) throw new ArgumentException(Strings.OutOfViewRange, "item"); else myBag.Add(item); } /// /// Searches the underlying bag for an item equal to , and if found, /// removes it from the bag. If not found, the bag is unchanged. If the item is outside /// the range of this view, the bag is unchanged. /// /// /// Equality between items is determined by the comparison instance or delegate used /// to create the bag. /// Removing an item from the bag takes time O(log N), where N is the number of items in the bag. /// The item to remove. /// True if was found and removed. False if was not in the bag, or /// was outside the range of this view. public sealed override bool Remove(T item) { if (!ItemInView(item)) return false; else return myBag.Remove(item); } /// /// Determines if this view of the bag contains an item equal to . The bag /// is not changed. If /// /// Searching the bag for an item takes time O(log N), where N is the number of items in the bag. /// The item to search for. /// True if the bag contains , and is within /// the range of this view. False otherwise. public sealed override bool Contains(T item) { if (!ItemInView(item)) return false; else return myBag.Contains(item); } /// /// Get the first index of the given item in the view. The smallest item in the view has index 0, /// the next smallest item has index 1, and the largest item has index Count-1. /// /// Finding the index takes time O(log N), which N is the number of items in /// the set. /// The item to get the index of. /// The index of the first item in the view equal to , or -1 if the item is not present /// in the view. public int IndexOf(T item) { if (entireTree) { if (reversed) { int indexInSet = myBag.tree.FindIndex(item, false); if (indexInSet < 0) return -1; return myBag.Count - 1 - indexInSet; } else { return myBag.tree.FindIndex(item, true); } } else { T dummy; if (!ItemInView(item)) return -1; if (reversed) { int indexInSet = myBag.tree.FindIndex(item, false); if (indexInSet < 0) return -1; int indexOfEnd = myBag.tree.LastItemInRange(rangeTester, out dummy); return indexOfEnd - indexInSet; } else { int indexInSet = myBag.tree.FindIndex(item, true); if (indexInSet < 0) return -1; int indexOfStart = myBag.tree.FirstItemInRange(rangeTester, out dummy); return indexInSet - indexOfStart; } } } /// /// Get the last index of the given item in the view. The smallest item in the view has index 0, /// the next smallest item has index 1, and the largest item has index Count-1. /// /// Finding the index takes time O(log N), which N is the number of items in /// the set. /// The item to get the index of. /// The index of the last item in the view equal to , or -1 if the item is not present /// in the view. public int LastIndexOf(T item) { if (entireTree) { if (reversed) { int indexInSet = myBag.tree.FindIndex(item, true); if (indexInSet < 0) return -1; return myBag.Count - 1 - indexInSet; } else { return myBag.tree.FindIndex(item, false); } } else { T dummy; if (!ItemInView(item)) return -1; if (reversed) { int indexInSet = myBag.tree.FindIndex(item, true); if (indexInSet < 0) return -1; int indexOfEnd = myBag.tree.LastItemInRange(rangeTester, out dummy); return indexOfEnd - indexInSet; } else { int indexInSet = myBag.tree.FindIndex(item, false); if (indexInSet < 0) return -1; int indexOfStart = myBag.tree.FirstItemInRange(rangeTester, out dummy); return indexInSet - indexOfStart; } } } /// /// Get the item by its index in the sorted order. The smallest item in the view has index 0, /// the next smallest item has index 1, and the largest item has index Count-1. /// /// The indexer takes time O(log N), which N is the number of items in /// the set. /// The index to get the item by. /// The item at the given index. /// is /// less than zero or greater than or equal to Count. public T this[int index] { get { if (entireTree) { if (reversed) { return myBag[myBag.Count - 1 - index]; } else { return myBag[index]; } } else { T dummy; int firstIndex = myBag.tree.FirstItemInRange(rangeTester, out dummy); int lastIndex = myBag.tree.LastItemInRange(rangeTester, out dummy); if (firstIndex < 0 || lastIndex < 0 || index < 0 || index >= (lastIndex - firstIndex + 1)) throw new ArgumentOutOfRangeException("index"); if (reversed) return myBag[lastIndex - index]; else return myBag[firstIndex + index]; } } } /// /// Get a read-only list view of the items in this view. The /// items in the list are in sorted order, with the smallest item /// at index 0. This view does not copy any data, and reflects any /// changes to the underlying OrderedSet. /// /// A read-only IList<T> view onto this view. public IList AsList() { return new ListView(myBag, rangeTester, entireTree, reversed); } /// /// Creates a new View that has the same items as this view, in the reversed order. /// /// A new View that has the reversed order of this view, with the same upper /// and lower bounds. public View Reversed() { return new View(myBag, rangeTester, entireTree, !reversed); } /// /// Returns the first item in this view: the item /// that would appear first if the view was enumerated. /// /// GetFirst() takes time O(log N), where N is the number of items in the bag. /// The first item in the view. /// The view has no items in it. public T GetFirst() { T item; int found; if (reversed) found = myBag.tree.LastItemInRange(rangeTester, out item); else found = myBag.tree.FirstItemInRange(rangeTester, out item); if (found < 0) throw new InvalidOperationException(Strings.CollectionIsEmpty); return item; } /// /// Returns the last item in the view: the item /// that would appear last if the view was enumerated. /// /// GetLast() takes time O(log N), where N is the number of items in the bag. /// The last item in the view. /// The view has no items in it. public T GetLast() { T item; int found; if (reversed) found = myBag.tree.FirstItemInRange(rangeTester, out item); else found = myBag.tree.LastItemInRange(rangeTester, out item); if (found < 0) throw new InvalidOperationException(Strings.CollectionIsEmpty); return item; } } #endregion } }