#include <utility> // for std::swap
void bubble_sort (int* A, size_t N) {
for (size_t i = N - 1; i >= 1; --i) {
for (size_t j = 0; j < i; ++j) {
if (A[j] > A[j + 1]) {
std::swap (A[j], A[j + 1]);
}
}
}
}
int a[10] = {9, 2, 10, 3, 1, 8, 4, 7, 5, 6};
bubble_sort (a, 10);
a
void bubble_sort_opt (int* A, size_t N) {
for (size_t i = N - 1; i >= 1; --i) {
bool didSwap = false;
for (size_t j = 0; j < i; ++j) {
if (A[j] > A[j + 1]) {
std::swap (A[j], A[j + 1]);
didSwap = true;
}
}
if (!didSwap) {
break;
}
}
}
int b[10] = {9, 2, 10, 3, 1, 8, 4, 7, 5, 6};
bubble_sort_opt (b, 10);
b
void insertion_sort (int* A, size_t N) {
for (size_t i = 1; i < N; ++i) {
// Invariant: A[0..i) is sorted
int v = A[i]; // element to place
int j = i; // Index to place 'v'
while (j >= 1 && v < A[j - 1]) {
A[j] = A[j - 1];
--j;
}
// Invariant: j == 0 or v >= A[j - 1]
A[j] = v;
}
}
int c[10] = {9, 2, 10, 3, 1, 8, 4, 7, 5, 6};
insertion_sort (c, 10);
c
void selection_sort (int* A, size_t N) {
// Find smallest and place. Repeat
for (size_t i = 0; i < N - 1; ++i) {
size_t min = i;
for (size_t j = i + 1; j < N; ++j) {
if (A[j] < A[min]) {
min = j;
}
}
/* if (i != min) */ {
std::swap (A[i], A[min]);
}
}
}
int d[10] = {9, 2, 10, 3, 1, 8, 4, 7, 5, 6};
selection_sort (d, 10);
d
std::find()
in <algorithm>
header
#include <iostream>
#include <algorithm>
int e[10] = {1, 10, 3, 8, 2, 4, 9, 5, 7, 6};
int val = 13;
int* pos = std::find (e, e + 10, val);
if (pos != e + 10) {
std::cout << *pos;
} else {
std::cout << "not found :(";
}
d
pos = std::lower_bound (d, d + 10, 13);
// *pos >= val or pos @ end
std::distance(d, pos)
std::binary_search (d, d + 10, 2)
Range Notation:
A[0..N)
A[0..N) <=> A[0..m) A[m..N)
Case 0: Not Found
if (first >= last) {
return -1;
}
Case 1: Match
if (target == midValue) {
return mid;
}
Case 2: Target smaller than middle value
if (target < midValue) {
/* reposition last to mid */
/* search subarray A[first .. mid) */
}
Case 3: Target larger than middle value
if (target > midValue) {
/* reposition first to mid + 1 */
/* search subarray A[mid + 1 .. last) */
}
Example: Successful Binary Search
0 1 2 3 4 5 6 7 8 9
+---+---+---+---+---+---+---+---+---+
|-7 | 3 | 5 | 8 | 12| 16| 23| 33| 55|
+---+---+---+---+---+---+---+---+---+
^ ^ ^
lo mid hi
target: 23 lo: 0 hi: 9
mid: halfway between first and last = 4
(target = 23) > (midvalue = 12)
--- Case 3
0 1 2 3 4 5 6 7 8 9
+---+---+---+---+---+---+---+---+---+
|-7 | 3 | 5 | 8 | 12| 16| 23| 33| 55|
+---+---+---+---+---+---+---+---+---+
^ ^ ^
lo mid hi
target: 23 lo: 5 hi: 9
mid: halfway between first and last = 7
(target = 23) < (midvalue = 33)
--- Case 2
0 1 2 3 4 5 6 7 8 9
+---+---+---+---+---+---+---+---+---+
|-7 | 3 | 5 | 8 | 12| 16| 23| 33| 55|
+---+---+---+---+---+---+---+---+---+
^ ^ ^
lo mid hi
target: 23 lo: 5 hi: 7
mid: halfway between first and last = 6
(target = 23) == (midvalue = 23)
--- Case 1
Example: Unsuccessful Binary Search
0 1 2 3 4 5 6 7 8 9
+---+---+---+---+---+---+---+---+---+
|-7 | 3 | 5 | 8 | 12| 16| 23| 33| 55|
+---+---+---+---+---+---+---+---+---+
^ ^ ^
lo mid hi
target: 4 lo: 0 hi: 9
mid: halfway between first and last = 4
(target = 4) < (midvalue = 12)
--- Case 2
0 1 2 3 4 5 6 7 8 9
+---+---+---+---+---+---+---+---+---+
|-7 | 3 | 5 | 8 | 12| 16| 23| 33| 55|
+---+---+---+---+---+---+---+---+---+
^ ^ ^
lo mid hi
target: 4 lo: 0 hi: 4
mid: halfway between first and last = 2
(target = 4) < (midvalue = 5)
--- Case 2
0 1 2 3 4 5 6 7 8 9
+---+---+---+---+---+---+---+---+---+
|-7 | 3 | 5 | 8 | 12| 16| 23| 33| 55|
+---+---+---+---+---+---+---+---+---+
^ ^ ^
lo mid hi
target: 4 lo: 0 hi: 2
mid: halfway between first and last = 1
(target = 4) > (midvalue = 3)
--- Case 3
0 1 2 3 4 5 6 7 8 9
+---+---+---+---+---+---+---+---+---+
|-7 | 3 | 5 | 8 | 12| 16| 23| 33| 55|
+---+---+---+---+---+---+---+---+---+
^
lo
mid
hi
target: 4 lo: 2 hi: 2
mid: halfway between first and last = 2
first >= last
--- Case 0
size_t binary_search (int const * A, size_t N, int target) {
size_t first = 0;
size_t last = N;
// Tests for a non-empty sublist
while (first < last) {
size_t mid = first + (last - first) / 2;
if (target == A[mid]) {
return mid;
} else if (target < A[mid]) {
last = mid;
} else {
first = mid + 1;
}
}
return -1;
}
int f[9] = {-7, 3, 5, 8, 12, 16, 23, 33, 55};
binary_search (f, 9, 5)
(int)binary_search (f, 9, 34)
binary_search (f, 9, 12)
binary_search (f, 9, -7)
$mid = first + \frac{last - first}{2}$
versus
$mid = \frac{first + last}{2}$
Knowing the upper and lower limits of a sorted range, we can:
[1, 10, 100, 1000, ...]
would not beExample:
Binary Search
$$mid = \frac{first + last}{2} = \frac{0 + 1,000}{2} = 500$$Interpolation Search (note: $last$ is inclusive)
$$mid = \frac{val - A[first]}{A[last] - A[first]} \times (last - first)$$$$mid = \frac{12,000 - 1,000}{1,000,000 - 1,000} \times (999 - 0) $$
$$ mid = \frac{11,000}{990,000} \times 999 = 11 $$
size_t interpolation_search (int const * A, size_t N, int target) {
size_t s = 0;
size_t e = N - 1;
if (target < A[s]) return -1;
if (target >= A[e]) s = e;
while (s < e) {
size_t loc = s + (e - s) * (target - A[s]) / (A[e] - A[s]);
if (target == A[loc]) {
return loc;
} else if (target < A[loc]) {
e = loc - 1;
} else {
s = loc + 1;
}
}
if (target == A[s]) {
return s;
}
return -1;
}
$f(N) \le c * g(N)$ when $N \ge n_0$
Example growth (runtime) functions:
Show $f_1(N) = O(N^2)$
Let $n_0 = 1$
$f_1(N) = 3N^2 + 50N + 120$
$f_1(N) \le 3N^2 + 50N^2 + 120N^2 = 173N^2$
Choose $c = 173$
Therefore $f_1(N) \le 173N^2 \Rightarrow O(N^2)$
Show $f_3(N) = O(1)$
Let $n_0 = 1, c = 6$
$f_3(N) = 5 \le 6 * 1)$
An algorithm is $O(1)$ (constant time) when its complexity is independent of input size
Example:
std::vector<T>::push_back(T const& value);
capacity() > size()
, push_back()
has $O(1)$ complexityAn algorithm is $O(N)$ (linear time) when its runtime is proportional to input size
Example:
Sequential search for minimum element in an array
Iterator std::min_element (Iterator begin, Iterator end, Value const& v);
Algorithms with running time $O(N^2)$ are quadratic
Algorithms with running time $O(N^3)$ are cubic
#include <cmath>
#include <array>
#include <functional>
#include <iostream>
#include <iomanip>
[]() {
using fn = std::function<long double (long double)>;
static std::array<std::string,6> const tags = {"n", "lg(n)", "n*lg(n)", "n^2", "n^3", "2^n"};
static std::array<int,8> const ns = {2, 4, 8, 16, 32, 128, 1024, 65536};
static std::array<fn,6> const fns = {
[] (long double n) { return n; },
[] (long double n) { return std::log(n) / std::log(2); },
[] (long double n) { return n * std::log(n) / std::log(2); },
[] (long double n) { return n * n; },
[] (long double n) { return n * n * n; },
[] (long double n) { return std::pow(2, n); }
};
const int W = 14;
for (auto const &tag : tags) {
std::cout << std::setw(W) << tag;
}
std::cout << '\n';
for (double n : ns) {
for (auto fn : fns) {
std::cout << std::setw(W) << fn(n);
}
std::cout << '\n';
}
}();
n lg(n) n*lg(n) n^2 n^3 2^n
2 1 2 4 8 4
4 2 8 16 64 16
8 3 24 64 512 256
16 4 64 256 4096 65536
32 5 160 1024 32768 4.29497e+09
128 7 896 16384 2.09715e+06 3.40282e+38
1024 10 10240 1.04858e+06 1.07374e+09 1.79769e+308
65536 16 1.04858e+06 4.29497e+09 2.81475e+14 inf
A function's logic could be appropriate for different types
int
s)¶void selection_sort (int* A, size_t N) {
...
int temp;
for (size_t i = 0; i < N - 1; ++i) {
...
if (A[j] < A[min]) {
...
}
...
}
}
std::string
s)¶void selection_sort (std::string* A, size_t N) {
...
std::string temp;
for (size_t i = 0; i < N - 1; ++i) {
...
if (A[j] < A[min]) {
...
}
...
}
}
T
s)¶template <typename T>
void selection_sort (T* A, size_t N) {
...
T temp;
for (size_t i = 0; i < N - 1; ++i) {
...
if (A[j] < A[min]) {
...
}
...
}
}
Key Insight: operator<
MUST be defined for type T
template
typename T
T
should be referred to as a type whenever it appearsNOTE: Always comes at the very beginning of any function or class definition
array
, vector
, list
, forward_list
, deque
(unordered_)(multi)set
, (unordered_)(multi)map
stack
, queue
, priority_queue
A stack allows access only at one end of the sequence, called top
#include <stack>
std::stack<char> s;
You insert into a stack using push()
s.push('G');
s.push('C');
s.push('D');
s.size()
If you want to access the element at the top of the stack, use top()
s.top()
Calling top()
doesn't change the stack! It returns a copy
s.top()
If you want to remove an element, use pop()
Problem: pop()
doesn't return anything!
s.pop() // [G, C, D] -- removes the D
So if you want the value before pop()
you must call top()
template <typename T, typename Container>
T pop(std::stack<T, Container>& s) {
T value = s.top();
s.pop();
return value;
}
pop(s)
A queue allows insertion to the back and removal from the front
#include <queue>
std::queue<int> q
front()
back()
size()
You insert into a queue using push()
(same as stack!)
q.push(3);
q.push(6);
q.push(2);
q.front()
q.back()
q.size()
If you want to remove an element, use pop()
Problem: pop()
doesn't return anything!
template <typename T, typename Container>
T pop(std::queue<T, Container>& q) {
T value = q.front();
q.pop();
return value;
}
pop(q)
pop(q)
A priority queue has a highest priority, first out protocol
#include <queue>
std::priority_queue<int> pq;
pq.push(3);
pq.push(6);
pq.push(2);
pq.top()
pq.pop();
pq.top()
A set maintains a collection of unique values called keys
#include <set>
#include <initializer_list>
std::set<int> set = {3, 2, 1, 9, 1, 4};
set
std::multiset<int> multiset = {3, 2, 1, 9, 1, 4};
multiset
A map maintains a collection of key-value pairs
#include <map>
#include <string>
using namespace std::literals::string_literals;
std::map<int, std::string> students =
{ {12, "Bob"s}, {30, "Alice"s}, {4, "Charlie"s}};
students
int arr[N]; // where N is a compile-time constant
std::cin >> N;
int A[N];
std::copy
algorithmint N = 4;
int A[N];
array
class¶C++11 array is more flexible than plain array, but not as flexible as a vector
#include <array>
constexpr int N = 10;
std::array<int, N> arr;
arr.size()
arr.fill(1337);
arr[5]
vector
overcomes the limitations of array
std::vector
API¶Defined in header <vector>
namespace std {
template <
typename T, // value type to store
typename Allocator = std::allocator<T> // how to allocate data
> class vector;
}
#include <iostream>
#include <vector>
template <typename T>
std::vector<T>
printSizeAndCapacity (std::vector<T>&& v) {
std::cout << "Size: " << v.size() << '\n';
std::cout << "Capacity: " << v.capacity() << '\n';
return v;
}
std::vector
Special Member Datatypes¶Name | Description |
---|---|
T |
template parameter for value type to store |
value_type |
alias to T |
size_type |
unsigned integer type (usually std::size_t ) |
difference_type |
signed integer type (usually std::ptrdiff_t ) |
reference |
alias to value_type& |
const_reference |
alias to value_type const& |
pointer |
usually just value_type* |
const_pointer |
usually just value_type const* |
iterator |
aliased to value_type* |
const_iterator |
aliased to value_type const* |
reverse_iterator |
std::reverse_iterator<iterator> |
const_reverse_iterator |
std::reverse_iterator<const_iterator> |
std::vector
Contructors¶vector ();
vector (size_type n);
vector (size_type n, T const& value);
template <typename InputIterator>
vector (InputIterator first, InputIterator last);
vector (std::initializer_list<T>);
vector()
printSizeAndCapacity(std::vector<int>{})
vector(size_type n)
printSizeAndCapacity(std::vector<int>(5))
vector(size_type n, T const& value)
printSizeAndCapacity(std::vector<int>(5, 2))
template <typename InputIterator>
vector(InputIterator first, InputIterator last);
[first, last)
vector(std::initializer_list<T> list);
printSizeAndCapacity(std::vector<int>{5, 4, 3, 2, 1})
std::vector
Access Operations¶reference back ();
reference front ();
reference operator[] (size_type i);
reference at (size_type i);
pointer data () noexcept;
std::vector
Access Operations (const
versions)¶const_reference back () const;
const_reference front () const;
const_reference operator[] (size_type i) const;
const_reference at (size_type i) const;
const_pointer data () const noexcept;
reference back ();
const_reference back() const;
const
version of back()
[] () -> int& {
static std::vector<int> a = {1, 2, 3};
return a.back();
}()
[] () -> int const& {
const static std::vector<int> a = {1, 2, 3};
return a.back();
}()
reference operator[] (size_type i);
reference at (size_type i);
operator[]
but does run-time bounds checking[] {
std::vector<int> a = {1, 2, 3};
return a[1];
}()
[] {
std::vector<int> a = {1, 2, 3};
return a.at(4);
}()
std::vector
Capacity Operations¶bool empty () const;
size_type size () const;
size_type capacity () const;
size_type max_size () const;
void shrink_to_fit ();
void reserve (size_type newcap);
bool empty() const;
true
IFF the vector's size is zero
size_type size () const
size_type capacity() const;
void shrink_to_fit();
capacity()
to size()
void reserve(size_type newcap);
capacity()
then new storage is allocated; otherwise, the method does nothingcapacity()
$\ge newcap$, size is unchangedstd::vector
Iterator Operations¶iterator begin () noexcept;
iterator end () noexcept;
reverse_iterator rbegin () noexcept;
reverse_iterator rend () noexcept;
const_iterator begin () const noexcept; // and cbegin()
const_iterator end () const noexcept; // and cend()
const_reverse_iterator rbegin () const noexcept; // and crbegin()
const_reverse_iterator rend () const noexcept; // and crend()
std::vector
Modifying Operations¶void clear();
void pop_back ();
iterator erase (...); // 2 versions
iterator insert (...); // 5 versions
void push_back (const_reference value); // and "move" variant
template <typename ... Args>
iterator emplace (const_iterator pos, Args&& ... value);
template <typename ... Args>
void emplace_back (Args&& ... value);
void resize(...); // 2 versions
void push_back (const_reference value);
void pop_back ();
iterator insert (const_iterator pos, const_reference value);
iterator erase (const_iterator pos);
iterator erase (const_iterator first, const_iterator last);
[first, last)
std::distance(first, last)
; the same number of elements are removed
void resize(size_type n);
T()
is filled for the increase in elements at the end. If the size is decreased, the original values at the front are retained.std::vector
¶vector
svector
objectspush_back
vs. operator[]
template <typename T>
void insertionSort (std::vector<T>& v) {
// Logic exactly the same as array version
// ...
// while (j >= 1 && temp < v[j - 1])
// ...
}
All container classes in C++ standard library are templates
Syntax:
template <typename T1, typename T2, ...>
class ClassName
{
...
};
template <typename T> class Store;
template <typename T>
class Store {
public:
Store (const T& item = T())
: m_value (item) { }
T getValue() const {
return m_value;
}
void setValue(const T& item) {
m_value = item;
}
private:
T m_value;
};
// Using the class:
Store<double> s;
s.setValue(4.3);
s.getValue()
Store<int> s2 (10);
s2.getValue()