I've been writing C++03 non-professionally for a long time, and I finally started to play around with C++11 and C++14. I decided to try my hand at implementing a data structure that I was previously unfamiliar with: a max heap.
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>
template<typename T, typename C>
class Heap
{
private:
std::vector<T> items;
C compare;
// Heap operations.
void heapify(size_t root = 0);
void siftDown(size_t node);
void siftUp(size_t node);
// Utilities to retrieve parent, left, and right node indices.
constexpr size_t parent(size_t node) const;
constexpr size_t left(size_t parent) const;
constexpr size_t right(size_t parent) const;
public:
Heap(C compare = C()): compare{compare} {}
Heap(const std::vector<T>& items, C compare = C()): items{items}, compare{compare} { heapify(); };
Heap(std::vector<T>&& items, C compare = C()): items{items}, compare{compare} { heapify(); }
template<typename Iterator>
Heap(Iterator begin, Iterator end, C compare = C()): items{begin, end}, compare{compare} { heapify(); };
Heap(const Heap&) = default;
Heap(Heap&&) = default;
~Heap() = default;
Heap& operator=(const Heap&) = default;
Heap& operator=(Heap&&) = default;
void push(const T& item);
T pop();
const T& top() const;
const T& item(size_t node) const;
constexpr size_t size() const;
};
// Heapify an unordered array in O(N). This can also be done in O(N log N) using repeated insertions.
template<typename T, typename C>
void Heap<T, C>::heapify(size_t root)
{
if (root >= size()) return;
size_t l = left(root), r = right(root);
heapify(l);
heapify(r);
siftDown(root);
}
// Sift an item down in O(log N).
template<typename T, typename C>
void Heap<T, C>::siftDown(size_t node)
{
size_t l = left(node), r = right(node);
// Find which item should be highest between this one and its children.
size_t higher = node;
if (l < size() && compare(items[l], items[higher])) higher = l;
if (r < size() && compare(items[r], items[higher])) higher = r;
// If one of its children should be higher, swap and continue to sift down.
if (higher != node)
{
std::swap(items[node], items[higher]);
siftDown(higher);
}
}
// Sift an item up in O(log N).
template<typename T, typename C>
void Heap<T, C>::siftUp(size_t node)
{
if (node == 0 || node >= size()) return;
const size_t p = parent(node);
// If this item should be higher than its parent, swap and continue to sift up.
if (compare(items[node], items[p]))
{
std::swap(items[node], items[p]);
siftUp(p);
}
}
template<typename T, typename C>
constexpr size_t Heap<T, C>::parent(size_t node) const
{
return node > 0 ? (node - 1) / 2 : node;
}
template<typename T, typename C>
constexpr size_t Heap<T, C>::left(size_t parent) const
{
return parent * 2 + 1;
}
template<typename T, typename C>
constexpr size_t Heap<T, C>::right(size_t parent) const
{
return parent * 2 + 2;
}
template<typename T, typename C>
void Heap<T, C>::push(const T& item)
{
// Insert as the last item.
items.push_back(item);
// Sift it up to re-establish heap property.
siftUp(size() - 1);
}
template<typename T, typename C>
T Heap<T, C>::pop()
{
if (size() == 0) return T();
T ret{top()};
// Move last item to the top, then reduce the array size by one.
items[0] = items[size() - 1];
items.pop_back();
// Sift this new top item down to re-establish heap property.
siftDown(0);
return ret;
}
template<typename T, typename C>
const T& Heap<T, C>::top() const
{
return items.front();
}
template<typename T, typename C>
const T& Heap<T, C>::item(size_t node) const
{
return items[node];
}
template<typename T, typename C>
constexpr size_t Heap<T, C>::size() const
{
return items.size();
}
template<typename T, typename C>
void print(std::ostream& stream, const Heap<T, C>& h, size_t root = 0, size_t level = 0)
{
if (root >= h.size()) return;
// Print left child.
print(stream, h, root * 2 + 1, level + 1);
stream << std::string(level * 4, ' ') << h.item(root) << "\n";
// Print right child.
print(stream, h, root * 2 + 2, level + 1);
}
template<typename T, typename C>
std::ostream& operator<<(std::ostream& stream, const Heap<T, C>& h)
{
print(stream, h);
return stream;
}
int main()
{
Heap<int, std::greater<int>> h;
std::vector<int> arr;
for (int i = 0; i < 10; ++i)
{
int x = rand() % 20;
h.push(x);
arr.push_back(x);
}
Heap<int, std::greater<int>> h2{arr.begin(), arr.end()};
std::cout << "Created via push():\n" << h;
std::cout << "Created via heapify():\n" << h2;
std::cout << "Popping top item..." << std::endl;
h.pop();
h2.pop();
std::cout << "Created via push():\n" << h;
std::cout << "Created via heapify():\n" << h2;
}
Mainly looking for feedback on:
- Am I using C++11/14 features correctly? Have I written anything that's redundant? Am I missing anything important?
- Is my max heap implementation correct? Is my time complexity analysis (see comments above implementations) correct?
- Have I followed best practices in implementing a generic container with a custom comparison function?
- Have I made any mistakes with regards to exception safety?
Aside from that, please feel free to completely rip the code apart. Give me your honest thoughts as if you're reviewing a coworker's checkin (and ignore the fact that if I was your coworker, I would just use std::priority_queue
instead).