- ArrayList
- A dynamic, contiguous collection of objects.
- Favor the generic
collection List
instead. - Hashtable
- Associative, unordered collection of key-value pairs of objects.
- Favor the generic
collection Dictionary
instead. - Queue
- First-in-first-out (FIFO) collection of objects.
- Favor the generic
collection Queue
instead. - SortedList
- Associative, ordered collection of key-value pairs of objects.
- Favor the generic
collection SortedList
instead. - Stack
- Last-in-first-out (LIFO) collection of objects.
- Favor the generic
collection Stack
instead. - ConcurrentQueue
- Thread-safe version of a queue (FIFO).
- For more information see: C#/.NET Little Wonders: The ConcurrentStack and ConcurrentQueue
- ConcurrentStack
- Thread-safe version of a stack (LIFO).
- For more information see: C#/.NET Little Wonders: The ConcurrentStack and ConcurrentQueue
- ConcurrentBag
- Thread-safe unordered collection of objects.
- Optimized for situations where a thread may be bother reader and writer.
- For more information see: C#/.NET Little Wonders: The ConcurrentBag and BlockingCollection
- ConcurrentDictionary
- Thread-safe version of a dictionary.
- Optimized for multiple readers (allows multiple readers under same lock).
- For more information see C#/.NET Little Wonders: The ConcurrentDictionary
- BlockingCollection
- Wrapper collection that implement producers & consumers paradigm.
- Readers can block until items are available to read.
- Writers can block until space is available to write (if bounded).
- For more information see C#/.NET Little Wonders: The ConcurrentBag and BlockingCollection
- Analyzing the Performance of Data Structures
he .NET
Base Class Library (BCL) has a wide array of collection classes at your
disposal which make it easy to manage collections of objects. While it's great
to have so many classes available, it can be daunting to choose the right
collection to use for any given situation. As hard as it may be, choosing the
right collection can be absolutely key to the performance and maintainability
of your application!
The Generic Collections: System.Collections.Generic namespace
The
generic collections were introduced in .NET 2.0 in
the System.Collections.Generic namespace. This is the main body of
collections you should tend to focus on first, as they will tend to suit 99%
of your needs right up front.
It is
important to note that the generic collections are unsynchronized. This
decision was made for performance reasons because depending on how you are
using the collections its completely possible that synchronization may not be
required or may be needed on a higher level than simple method-level
synchronization. Furthermore, concurrent read access (all writes done at
beginning and never again) is always safe, but for concurrent mixed access you
should either synchronize the collection or use one of the concurrent
collections.
The Associative Collection Classes
Associative
collections store a value in the collection by providing a key that is used to
add/remove/lookup the item. Hence, the container associates the value with the
key. These collections are most useful when you need to lookup/manipulate a
collection using a key value. For example, if you wanted to look up an order
in a collection of orders by an order id, you might have an associative
collection where they key is the order id and the value is the order.
The Dictionary (hash table ) is probably the most used associative
container class. The Dictionary is the fastest
class for associative lookups/inserts/deletes because it uses a hash table
under the covers. Because the keys are hashed, the key type should correctly
implement GetHashCode() and Equals() appropriately or you should
provide an external IEqualityComparer to
the dictionary on construction. The insert/delete/lookup time of items in the
dictionary is amortized constant time - O(1) - which means no matter how big
the dictionary gets, the time it takes to find something remains relatively
constant. This is highly desirable for high-speed lookups. The only downside
is that the dictionary, by nature of using a hash table, is unordered, so you
cannot easily traverse the items in a Dictionary in order.
The SortedDictionary (binary
tree) is similar to the Dictionary in
usage but very different in implementation. The
SortedDictionary uses a binary tree under the covers
to maintain the items in order by the key. As a consequence of sorting, the
type used for the key must correctly implement IComparable so that the keys can be correctly
sorted. The sorted dictionary trades a little bit of lookup time for the
ability to maintain the items in order, thus insert/delete/lookup times in a
sorted dictionary are logarithmic - O(log n). Generally speaking, with
logarithmic time, you can double the size of the collection and it only has to
perform one extra comparison to find the item. Use
the SortedDictionary when you want fast lookups
but also want to be able to maintain the collection in order by the key.
The SortedList (sorted array)
is the other sorted associative container class in the generic containers.
Once again SortedList,
like SortedDictionary, uses a key to sort key-value
pairs. Unlike SortedDictionary, however, items in a SortedList are
stored as sorted array of items. This means that insertions and deletions are
linear - O(n) - because deleting or adding an item may involve shifting all
items up or down in the list. Lookup time, however is O(log n) because the SortedList can
use a binary search to find any item in the list by its key. So why would you
ever want to do this? Well, the answer is that if you are going to load
the SortedList up-front, the insertions will be slower, but because
array indexing is faster than following object links, lookups are marginally
faster than a SortedDictionary. Once again I'd use this in situations
where you want fast lookups and want to maintain the collection in order by
the key, and where insertions and deletions are rare.
The Non-Associative Containers
The
other container classes are non-associative. They don't use keys to manipulate
the collection but rely on the object itself being stored or some other means
(such as index) to manipulate the collection.
The List (dynamic array) is a basic
contiguous storage container. Some people may call this a vector or dynamic
array. Essentially it is an array of items that grow once its current capacity
is exceeded. Because the items are stored contiguously as an array, you can
access items in the List by index very quickly. However
inserting and removing in the beginning or middle of
the List are very costly because you must shift all the
items up or down as you delete or insert respectively. However, adding and
removing at the end of a List is an amortized constant
operation - O(1). Typically List is the standard go-to
collection when you don't have any other constraints, and typically we favor
a List even over arrays unless we are sure the size will
remain absolutely fixed.
The LinkedList (doubly-linked list) is
a basic implementation of a doubly-linked list. This means that you can add or
remove items in the middle of a linked list very quickly (because there's no
items to move up or down in contiguous memory), but you also lose the ability
to index items by position quickly. Most of the time we tend to
favor List over LinkedList unless you
are doing a lot of adding and removing from the collection, in which case a
LinkedList may make more
sense.
The HashSet is an unordered
collection of unique items. This means that the collection cannot have
duplicates and no order is maintained. Logically, this is very similar to
having a Dictionary where
the TKey and TValue both refer to the same object. This
collection is very useful for maintaining a collection of items you wish to
check membership against. For example, if you receive an order for a given
vendor code, you may want to check to make sure the vendor code belongs to the
set of vendor codes you handle. In these cases a HashSet is
useful for super-quick lookups where order is not important. Once again, like
in Dictionary, the type T should have a valid implementation of GetHashCode()and Equals(), or you should provide an appropriate IEqualityComparer to the HashSet on construction.
The SortedSet (binary tree) is
to HashSet what
the SortedDictionary is
to Dictionary. That is, the SortedSetis
a binary tree where the key and value are the same object. This once again
means that adding/removing/lookups are logarithmic - O(log n) - but you gain
the ability to iterate over the items in order. For this collection to be
effective, type T must implement IComparable or
you need to supply an external IComparer .
Finally,
the Stack and Queue are two very specific
collections that allow you to handle a sequential collection of objects in
very specific ways. The Stack is
a last-in-first-out (LIFO) container where items are added and removed from
the top of the stack. Typically this is useful in situations where you want to
stack actions and then be able to undo those actions in reverse order as
needed. The Queue on
the other hand is a first-in-first-out container which adds items at the end
of the queue and removes items from the front. This is useful for situations
where you need to process items in the order in which they came, such as a
print spooler or waiting lines.
So
that's the basic collections. Let's summarize what we've learned in a quick
reference table.
Collection
|
Ordering
|
Contiguous
Storage?
|
Direct
Access?
|
Lookup
Efficiency
|
Manipulate
Efficiency
|
Notes
|
Dictionary
|
Unordered
|
Yes
|
Via Key
|
Key:
O(1)
|
O(1)
|
Best
for high performance lookups.
|
SortedDictionary
|
Sorted
|
No
|
Via
Key
|
Key:
O(log
n)
|
O(log
n)
|
Compromise
of Dictionary speed and ordering, uses binarysearch tree.
|
SortedList
|
Sorted
|
Yes
|
Via
Key
|
Key:
O(log
n)
|
O(n)
|
Very
similar toSortedDictionary, except tree is implemented in an array, so has
faster lookup on preloaded data, but slower loads.
|
List
|
User
has precise control over element ordering
|
Yes
|
Via
Index
|
Index:
O(1)
Value:
O(n)
|
O(n)
|
Best
for smaller lists where direct access required and no sorting.
|
LinkedList
|
User
has precise control over element ordering
|
No
|
No
|
Value:
O(n)
|
O(1)
|
Best
for lists where inserting/deleting in middle is common and no direct
access required.
|
HashSet
|
Unordered
|
Yes
|
Via
Key
|
Key:
O(1)
|
O(1)
|
Unique
unordered collection, like a Dictionary except key and value are same
object.
|
SortedSet
|
Sorted
|
No
|
Via
Key
|
Key:
O(log
n)
|
O(log
n)
|
Unique
sorted collection, like SortedDictionary except key and value are same
object.
|
Stack
|
LIFO
|
Yes
|
Only
Top
|
Top:
O(1)
|
O(1)*
|
Essentially
same as List
|
Queue
|
FIFO
|
Yes
|
Only
Front
|
Front: O(1)
|
O(1)
|
Essentially same
as List
|
The Original Collections: System.Collections namespace
The
original collection classes are largely considered deprecated by developers
and by Microsoft itself. In fact they indicate that for the most part you
should always favor the generic or concurrent collections, and only use the
original collections when you are dealing with legacy .NET code.
Because
these collections are out of vogue, let's just briefly mention the original
collection and their generic equivalents:
In
general, the older collections are non-type-safe and in some cases less
performant than their generic counterparts. Once again, the only reason you
should fall back on these older collections is for backward compatibility with
legacy code and libraries only.
The Concurrent Collections: System.Collections.Concurrent
namespace
The
concurrent collections are new as of .NET 4.0 and are included in
the System.Collections.Concurrent namespace. These collections are
optimized for use in situations where multi-threaded read and write access of
a collection is desired.
The
concurrent queue, stack, and dictionary work much as you'd expect. The bag and
blocking collection are more unique. Below is the summary of each with a link
to a blog post I did on each of them.
Summary
The
.NET BCL has lots of collections built in to help you store and manipulate
collections of data. Understanding how these collections work and knowing in
which situations each container is best is one of the key skills necessary to
build more performant code. Choosing the wrong collection for the job can make
your code much slower or even harder to maintain if you choose one that
doesn’t perform as well or otherwise doesn’t exactly fit the situation.
Remember
to avoid the original collections and stick with the generic
collections. If you need concurrent access, you can use the generic
collections if the data is read-only, or consider the concurrent collections
for mixed-access if you are running on .NET 4.0 or higher.
-----
(code for Class design for most of the adv collection classes)
The term "running time," despite its name, does not
measure the absolute time it takes the program to run, but rather refers to
the number of steps the program must perform to complete the given task at
hand. When working with arrays, typically the steps considered are how many
array accesses one needs to perform.
The
time it takes to search an array is linearly proportional to the number of
elements in the array.
This
sort of analysis described here is called asymptotic
analysis, as it
examines how the efficiency of a data structure changes as the data
structure's size approaches infinity. The notation commonly used in asymptotic
analysis is called big-Oh notation. The big-Oh notation to describe
the performance of searching an unsorted array would be denoted as O(n). The large script O is where the terminology
big-Oh notation comes from, and the n indicates that the number
of steps required to search an array grows linearly as the size of the array
grows.
Steps : if any step
is in loop and time it takes is n - it's in two loops it's time taken is n*n.
To search an array
time taken is "n" - Running
time = o(n).
lower
the term inside the parenthesis for
large values of n, the better the data structure's operation's
performance. asymptotic running time should be analyzed in practical
scenario, it might be possible that data is less so even if o(n) is small,
actual performance is less.
Array:
Stores homogenous
type or derived type. Memory is contiguous for array elements.
The
running time of an array access (array[7] ) is denoted O(1) , irrespective of the
size.
Search time for
simple array: O(n),
Look up time :
O(1).
search time for two
dimension array : o(n*n)
Homogenous and
fixed size.
Sorted search O(Log
n)
ArrayList :
Built on top of
Array with heterogeneous data type and resizing capabilities (through reallocation).
Arraylist is an array of objects and all types derived from object so any data
type can be saved. But has cost of casting. Specially with value type as in
arraylist value types are stored as Boxed value type i.e array stores a
reference and that point to actual value type. While in array the value type
is stored as unboxed value type (so no redirection is required). So cost is of
boxing/unboxing as well as of redirection.
Binary search
required values to be in sorted order if arraylist is not sorted binary search
will give wrong output.
List
List is also built
on top of array and it's generic so performance is same and no overhead.
The Queue and Stack differ from the List class in that there are
limitations on how the Queue and Stack data can be accessed.
Requirement is to
buffer (queue) the jobs for further processing - class with list
and nextjobindex is inefficient because
processed job memory is not reclaimed and can't be done as list is
linear structure and nextJobIndex wouldn't work if we store new jobs on free
available space.
Queue
is like circular array, with start and end pointers,Enqueue() and Dequque() methods and growth factor which increase the size of
queue for growthfactor=2, size get double when there is new job and no space.
Jobs can be assessed in FIFO order and can't be done randomly but it has contains
method to find if there is element.
Opposite is Stack -
First in last out LIFO- it has push and pop methods. It can be visualized as vertical
collection of items. (used in stack trace debugger)
The asymptotic
running time of the List's operations are the same as those of the standard
array's. While the List does indeed have more overhead, the relationship
between the number of elements in the List and the cost per operation is the
same as the standard array.
HashTable:
Stores item against
hashkey. To generate Hashkey .Net uses object's GetHashCode function.
There is
posibbility that two objects generates the same hashcode so there are
approaches for conflict resolution. Resolution is costly so all care is taken
to generate the unique hashcodes.
Linear Probing -
Technique where if generated hashcode already occupied use the next available
slot.
Rehashing or double
hashing - .Net hashtable uses this approach, in this couple of hash functions
are used which are differ just in some multiplication factor, so first H1 will
be used incase it's duplicate H2 will be used.
The Hashtable class contains a private
member variable called loadFactor that specifies the maximum ratio of items in the
Hashtable to the total number of slots. A loadFactor of, say, 0.5, indicates
that at most the Hashtable can only have half of its slots filled with items
and the other half must remain empty. Load factor of 1 translate to 0.72 in
.Net (internal to hashtable).
Finally, realize
that expanding the Hashtable is not an inexpensive operation. Therefore, if
you have an estimate as to how many items you're Hashtable will end up
containing, you should set the Hashtable's initial capacity accordingly in the
constructor so as to avoid unnecessary expansions.
The
asymptotic access time for a Hashtable is O(1), which beats the pants off of
the O(n) search time for an array.
The Dictionary class is a type-safe
Hashtable implementation, and strongly types both the keys and values.
Dictionary used different conflict resolution approach called Chaining. In these any conflicts are
stored in chain which is a secondary data type and is lnked to hash key in the
dictionary.
Binary
Trees: [ only one root / only one
parent / no cycles ]
the
various BinaryTreeNode instances that makeup a binary tree can be
scattered throughout the CLR managed heap. They are not necessarily contiguous, as are the
elements of an array.
There's no direct
access to a given node as with an array. Searching a binary tree can take
linear time, as potentially all nodes will need to be examined. That is, as
the number of nodes in the binary tree increases, the number of steps to find
an arbitrary node will increase as well.
So, if a binary
tree's lookup time is linear, and its search time is linear, how is the binary
tree any better than an array, whose search time is linear, but whose lookup
time is constant? Well, a generic binary tree doesn't offer us any benefit
over an array. However, by intelligently organizing the items in a binary
tree, we can greatly improve the search time (and therefore the lookup time as
well).
Organized binary
tree efficient for search are called - Binary
Search Trees (BSTs) : The principle of data storage for BSTs is that - For
each node data on the left is less than the node and data on the right is
greater than the node.
The
important concept to understand here is that ideally at each step in the
algorithm the number of nodes that have to be considered has been cut in
half. Searching a binary tree is similar in analysis to searching a
sorted array. Such algorithms that exhibit this property have an asymptotic
running time of log2 n, commonly abbreviated lg n.
The
time it takes to search a BST is dependent upon its topology. In the best
case, the time is on the order of log2 n, but in the worst case it
requires linear time.
Since newly
inserted nodes into a BST are inserted as leaves, the order of insertion
directly affects the topology of the BST itself. For example, imagine we
insert the following nodes into a BST: 1, 2, 3, 4, 5, and 6. When 1 is
inserted, it is insert as the root. Next, 2 is inserted as 1's right child. 3
is inserted as 2's right child, 4 as 3's right child, and so on. This result
in linear BST with worst search time o(n)
Deleting nodes from
a BST is slightly more difficult than inserting a node because deleting a node
that has children requires that some other node be chosen to replace the hole
created by the deleted node.
A balanced tree is a tree that maintains some predefined ratio between
its height and breadth. Different data structures define their own ratios for
balance, but all have it close to log2 n. A self-balancing BST, then, exhibits log2 n asymptotic running time.
There are numerous self-balancing BST data structures in existence, such as
AVL trees, red-black trees, 2-3 trees, 2-3-4 trees, splay trees, B-trees, and
others.
LinkedList
Recall
that with a binary tree, each node in the tree contains some bit of data and a
reference to its left and right children. A linked list can be thought of as a
unary tree. That is, each element in a linked list has some data associated
with it, and a single reference to its neighbor.
Linked lists have
the same linear running time for searches as arrays.
The
asymptotic time required to insert a new element into a linked list depends on
whether or not the linked list is a sorted list. The primary benefit of linked
lists is that adding or removing items does not involve messy and
time-consuming resizing. Granted, the List class hides the code
complexity of this, but resizing still carries with it a performance penalty.
Generic linklist class implements a doubly-linked list, which is a linked list whose
nodes have a reference to both their next neighbor and their previous
neighbor.
Skip Lists: A Linked List
with Self-Balancing BST-Like Properties
almost always use
Dictionary for lookups
On the other hand, it is faster to loop
through all the elements in a List than in a Dictionary.
Pasted
from <http://www.dotnetperls.com/dictionary>
Pasted
from <http://www.dotnetperls.com/dictionary>
No comments:
Post a Comment