As review, containers are objects that store a variable amount of data items, allowing for control and protection of the data. It's easy to copy/edit/sort/order many objects at once.
This makes it easy to create more complex data structures, by using containers within containers (databases are really just fancy containers!).
There are different categories of containers:
add()
and remove()
find()
Interface | Sorted | Ordered |
---|---|---|
addElement(val) |
Override | Inherited |
remove(val) |
Inherited | Inherited |
isMember(val) |
Inherited | Inherited |
find(val) |
Inherited | Inherited |
findPosition(val) |
Yes | Yes |
operator[]() |
Yes | Yes |
withdraw(iterator) |
Yes | Yes |
insertAfter() |
No | Yes |
insertBefore() |
No | Yes |
Like most container structures, they can be implemented with an array<>
or a list<>
. Each has their own complexities, and so it is useful to compare them.
Ordered container
Interface | Array | Linked List |
---|---|---|
addElement(val) |
\(O(1)\) | \(O(1)\) |
remove(val) |
\(O(n)\) | \(O(n)\) |
remove(iterator) |
\(O(n)\) | \(O(n) \text{ or } O(1)\) |
isMember(val) |
\(O(n)\) | \(O(n)\) |
find(val) |
\(O(n)\) | \(O(n)\) |
findPosition(val) |
\(O(n)\) | \(O(n)\) |
iterator operator*() |
\(O(1)\) | \(O(1)\) |
operator[](unsigned) |
\(O(1)\) | \(O(n)\) |
insertAfter |
\(O(n)\) | \(O(1)\) |
insertBefore() |
\(O(n)\) | \(O(n) \text{ or } O(1)\) |
Sorted container
Interface | Array | Linked List |
---|---|---|
addElement(val) |
\(O(n)\) | \(O(n)\) |
remove(val) |
\(O(n)\) | \(O(n)\) |
remove(iterator) |
\(O(n)\) | \(O(n) \text{ or } O(1)\) |
isMember(val) |
\(O(\log n)\) | \(O(n)\) |
find(val) |
\(O(\log n)\) | \(O(n)\) |
findPosition(val) |
\(O(\log n)\) | \(O(n)\) |
iterator operator*() |
\(O(1)\) | \(O(1)\) |
operator[](unsigned) |
\(O(1)\) | \(O(n)\) |
Binary search is a fast type of search (typically \(O(\log n)\)), where you find an element in the middle of the sorted data structure. If what you're looking for is less than the middle element, perform binary search on the less side. If what you're looking for is greater than the middle element, perform binary search on the greater side. Else, return the position of the element.
In C++, this is done using the binary_search()
function. However, this returns a bool
, not the location. If you want a location, use the functions lower_bound()
, upper_bound()
, and equal_range()
.
In searching and sorting, you need to have comparators. These are typically implemented as functors. For example, let's say you have a struct Point:
struct Point {
int x, y;
Point() : x(-1), y(-1) {}
Point(int xx, int yy) : x(xx), y(yy) {}
};
You need to have some way to tell if one Point is greater or less than another Point. Here, you could either compare by x or y, so one could write two functors to do that:
struct CompareByX {
bool operator()(const Point &p1, const Point &p2) const {
return p1.x > p2.x;
}
};
struct CompareByY {
bool operator()(const Point &p1, const Point &p2) const {
return p1.y > p2.y;
}
};
A set is a type of mathematical structure that allows you to have a collection of elements, with no duplicates. There are well-defined operations for sets, implemented in the STL as well:
Method | Asymptotic Complexity |
---|---|
initialize() |
\(O(n)\) |
clear() |
\(O(1) \text{ or } O(n)\) |
isMember() |
\(O(\log n)\) |
copy() |
\(O(n)\) |
set_union() |
\(O(n)\) |
set_intersection() |
\(O(n)\) |
Sets of integers can be implemented using a vector<bool>
or bitset<>
, where the member testing takes constant time.