Added log(n) move-to-front implementation
authorAndrey Tuganov <andreyt@google.com>
Mon, 12 Jun 2017 16:01:39 +0000 (12:01 -0400)
committerDavid Neto <dneto@google.com>
Thu, 29 Jun 2017 20:16:18 +0000 (16:16 -0400)
The implementation is based on AVL and order statistic tree.

It accepts all kinds of values and the implementation
doesn't expect the behaviour to be consistent with id coding.

Intended by SPIR-V compression algorithms.

source/util/move_to_front.h [new file with mode: 0644]
test/CMakeLists.txt
test/move_to_front_test.cpp [new file with mode: 0644]

diff --git a/source/util/move_to_front.h b/source/util/move_to_front.h
new file mode 100644 (file)
index 0000000..dc1430f
--- /dev/null
@@ -0,0 +1,649 @@
+// Copyright (c) 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef LIBSPIRV_UTIL_MOVE_TO_FRONT_H_
+#define LIBSPIRV_UTIL_MOVE_TO_FRONT_H_
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <iomanip>
+#include <iostream>
+#include <ostream>
+#include <sstream>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace spvutils {
+
+// Log(n) move-to-front implementation. Implements the following functions:
+// Insert - pushes value to the front of the mtf sequence
+//          (only unique values allowed).
+// Remove - remove value from the sequence.
+// ValueFromRank - access value by its 1-indexed rank in the sequence.
+// RankFromValue - get the rank of the given value in the sequence.
+// Accessing a value with ValueFromRank or RankFromValue moves the value to the
+// front of the sequence (rank of 1).
+//
+// The implementation is based on an AVL-based order statistic tree. The tree
+// is ordered by timestamps issued when values are inserted or accessed (recent
+// values go to the left side of the tree, old values are gradually rotated to
+// the right side).
+//
+// Terminology
+// rank: 1-indexed rank showing how recently the value was inserted or accessed.
+// node: handle used internally to access node data.
+// size: size of the subtree of a node (including the node).
+// height: distance from a node to the farthest leaf.
+template <typename Val>
+class MoveToFront {
+ public:
+  explicit MoveToFront(size_t reserve_capacity = 128) {
+    nodes_.reserve(reserve_capacity);
+
+    // Create NIL node.
+    nodes_.emplace_back(Node());
+  }
+
+  virtual ~MoveToFront() {}
+
+  // Inserts value in the move-to-front sequence. Does nothing if the value is
+  // already in the sequence. Returns true if insertion was successful.
+  // The inserted value is placed at the front of the sequence (rank 1).
+  bool Insert(const Val& value);
+
+  // Removes value from move-to-front sequence. Returns false iff the value
+  // was not found.
+  bool Remove(const Val& value);
+
+  // Computes 1-indexed rank of value in the move-to-front sequence and moves
+  // the value to the front. Example:
+  // Before the call: 4 8 2 1 7
+  // RankFromValue(8) returns 1
+  // After the call: 8 4 2 1 7
+  // Returns true iff the value was found in the sequence.
+  bool RankFromValue(const Val& value, size_t* rank);
+
+  // Returns value corresponding to a 1-indexed rank in the move-to-front
+  // sequence and moves the value to the front. Example:
+  // Before the call: 4 8 2 1 7
+  // ValueFromRank(1) returns 8
+  // After the call: 8 4 2 1 7
+  // Returns true iff the rank is within bounds [1, GetSize()].
+  bool ValueFromRank(size_t rank, Val* value);
+
+  // Returns the number of elements in the move-to-front sequence.
+  size_t GetSize() const {
+    return SizeOf(root_);
+  }
+
+ protected:
+  // Internal tree data structure uses handles instead of pointers. Leaves and
+  // root parent reference a singleton under handle 0. Although dereferencing
+  // a null pointer is not possible, inappropriate access to handle 0 would
+  // cause an assertion. Handles are not garbage collected if value was deprecated
+  // with DeprecateValue(). But handles are recycled when a node is repositioned.
+
+  // Internal tree data structure node.
+  struct Node {
+    // Timestamp from a logical clock which updates every time the element is
+    // accessed through ValueFromRank or RankFromValue.
+    uint32_t timestamp = 0;
+    // The size of the node's subtree, including the node.
+    // SizeOf(LeftOf(node)) + SizeOf(RightOf(node)) + 1.
+    uint32_t size = 0;
+    // Handles to connected nodes.
+    uint32_t left = 0;
+    uint32_t right = 0;
+    uint32_t parent = 0;
+    // Distance to the farthest leaf.
+    // Leaves have height 0, real nodes at least 1.
+    uint32_t height = 0;
+    // Stored value.
+    Val value = Val();
+  };
+
+  // Creates node and sets correct values. Non-NIL nodes should be created only
+  // through this function. If the node with this value has been created previously
+  // and since orphaned, reuses the old node instead of creating a new one.
+  uint32_t CreateNode(uint32_t timestamp, const Val& value) {
+    uint32_t handle = static_cast<uint32_t>(nodes_.size());
+    const auto result = value_to_node_.emplace(value, handle);
+    if (result.second) {
+      // Create new node.
+      nodes_.emplace_back(Node());
+      Node& node = nodes_.back();
+      node.timestamp = timestamp;
+      node.value = value;
+      node.size = 1;
+      // Non-NIL nodes start with height 1 because their NIL children are leaves.
+      node.height = 1;
+    } else {
+      // Reuse old node.
+      handle = result.first->second;
+      assert(!IsInTree(handle));
+      assert(ValueOf(handle) == value);
+      assert(SizeOf(handle) == 1);
+      assert(HeightOf(handle) == 1);
+      MutableTimestampOf(handle) = timestamp;
+    }
+
+    return handle;
+  }
+
+  // Node accessor methods. Naming is designed to be similar to natural
+  // language as these functions tend to be used in sequences, for example:
+  // ParentOf(LeftestDescendentOf(RightOf(node)))
+
+  // Returns value of the node referenced by |handle|.
+  Val ValueOf(uint32_t node) const {
+    return nodes_.at(node).value;
+  }
+
+  // Returns left child of |node|.
+  uint32_t LeftOf(uint32_t node) const {
+    return nodes_.at(node).left;
+  }
+
+  // Returns right child of |node|.
+  uint32_t RightOf(uint32_t node) const {
+    return nodes_.at(node).right;
+  }
+
+  // Returns parent of |node|.
+  uint32_t ParentOf(uint32_t node) const {
+    return nodes_.at(node).parent;
+  }
+
+  // Returns timestamp of |node|.
+  uint32_t TimestampOf(uint32_t node) const {
+    assert(node);
+    return nodes_.at(node).timestamp;
+  }
+
+  // Returns size of |node|.
+  uint32_t SizeOf(uint32_t node) const {
+    return nodes_.at(node).size;
+  }
+
+  // Returns height of |node|.
+  uint32_t HeightOf(uint32_t node) const {
+    return nodes_.at(node).height;
+  }
+
+  // Returns mutable reference to value of |node|.
+  Val& MutableValueOf(uint32_t node) {
+    assert(node);
+    return nodes_.at(node).value;
+  }
+
+  // Returns mutable reference to handle of left child of |node|.
+  uint32_t& MutableLeftOf(uint32_t node) {
+    assert(node);
+    return nodes_.at(node).left;
+  }
+
+  // Returns mutable reference to handle of right child of |node|.
+  uint32_t& MutableRightOf(uint32_t node) {
+    assert(node);
+    return nodes_.at(node).right;
+  }
+
+  // Returns mutable reference to handle of parent of |node|.
+  uint32_t& MutableParentOf(uint32_t node) {
+    assert(node);
+    return nodes_.at(node).parent;
+  }
+
+  // Returns mutable reference to timestamp of |node|.
+  uint32_t& MutableTimestampOf(uint32_t node) {
+    assert(node);
+    return nodes_.at(node).timestamp;
+  }
+
+  // Returns mutable reference to size of |node|.
+  uint32_t& MutableSizeOf(uint32_t node) {
+    assert(node);
+    return nodes_.at(node).size;
+  }
+
+  // Returns mutable reference to height of |node|.
+  uint32_t& MutableHeightOf(uint32_t node) {
+    assert(node);
+    return nodes_.at(node).height;
+  }
+
+  // Returns true iff |node| is left child of its parent.
+  bool IsLeftChild(uint32_t node) const {
+    assert(node);
+    return LeftOf(ParentOf(node)) == node;
+  }
+
+  // Returns true iff |node| is right child of its parent.
+  bool IsRightChild(uint32_t node) const {
+    assert(node);
+    return RightOf(ParentOf(node)) == node;
+  }
+
+  // Returns true iff |node| has no relatives.
+  bool IsOrphan(uint32_t node) const {
+    assert(node);
+    return !ParentOf(node) && !LeftOf(node) && !RightOf(node);
+  }
+
+  // Returns true iff |node| is in the tree.
+  bool IsInTree(uint32_t node) const {
+    assert(node);
+    return node == root_ || !IsOrphan(node);
+  }
+
+  // Returns the height difference between right and left subtrees.
+  int BalanceOf(uint32_t node) const {
+    return int(HeightOf(RightOf(node))) - int(HeightOf(LeftOf(node)));
+  }
+
+  // Updates size and height of the node, assuming that the children have
+  // correct values.
+  void UpdateNode(uint32_t node);
+
+  // Returns the most LeftOf(LeftOf(... descendent which is not leaf.
+  uint32_t LeftestDescendantOf(uint32_t node) const {
+    uint32_t parent = 0;
+    while (node) {
+      parent = node;
+      node = LeftOf(node);
+    }
+    return parent;
+  }
+
+  // Returns the most RightOf(RightOf(... descendent which is not leaf.
+  uint32_t RightestDescendantOf(uint32_t node) const {
+    uint32_t parent = 0;
+    while (node) {
+      parent = node;
+      node = RightOf(node);
+    }
+    return parent;
+  }
+
+  // Inserts node in the tree. The node must be an orphan.
+  void InsertNode(uint32_t node);
+
+  // Removes node from the tree. May change value_to_node_ if removal uses a
+  // scapegoat. Returns the removed (orphaned) handle for recycling. The
+  // returned handle may not be equal to |node| if scapegoat was used.
+  uint32_t RemoveNode(uint32_t node);
+
+  // Rotates |node| left, reassigns all connections and returns the node
+  // which takes place of the |node|.
+  uint32_t RotateLeft(const uint32_t node);
+
+  // Rotates |node| right, reassigns all connections and returns the node
+  // which takes place of the |node|.
+  uint32_t RotateRight(const uint32_t node);
+
+  // Root node handle. The tree is empty if root_ is 0.
+  uint32_t root_ = 0;
+
+  // Incremented counters for next timestamp and value.
+  uint32_t next_timestamp_ = 1;
+
+  // Holds all tree nodes. Indices of this vector are node handles.
+  std::vector<Node> nodes_;
+
+  // Maps ids to node handles.
+  std::unordered_map<Val, uint32_t> value_to_node_;
+};
+
+template <typename Val>
+bool MoveToFront<Val>::Insert(const Val& value) {
+  auto it = value_to_node_.find(value);
+  if (it != value_to_node_.end() && IsInTree(it->second))
+    return false;
+
+  const size_t old_size = GetSize();
+  (void)old_size;
+
+  InsertNode(CreateNode(next_timestamp_++, value));
+
+  assert(value_to_node_.count(value));
+  assert(old_size + 1 == GetSize());
+  return true;
+}
+
+template <typename Val>
+bool MoveToFront<Val>::Remove(const Val& value) {
+  auto it = value_to_node_.find(value);
+  if (it == value_to_node_.end())
+    return false;
+
+  if (!IsInTree(it->second))
+    return false;
+
+  const uint32_t orphan = RemoveNode(it->second);
+  (void)orphan;
+  // The node of |value| is still alive but it's orphaned now. Can still be
+  // reused later.
+  assert(!IsInTree(orphan));
+  assert(ValueOf(orphan) == value);
+  return true;
+}
+
+template <typename Val>
+bool MoveToFront<Val>::RankFromValue(const Val& value, size_t* rank) {
+  const size_t old_size = GetSize();
+  (void)old_size;
+  const auto it = value_to_node_.find(value);
+  if (it == value_to_node_.end()) {
+    return false;
+  }
+
+  uint32_t target = it->second;
+
+  if (!IsInTree(target)) {
+    return false;
+  }
+
+  uint32_t node = target;
+  *rank = 1 + SizeOf(LeftOf(node));
+  while (node) {
+    if (IsRightChild(node))
+      *rank += 1 + SizeOf(LeftOf(ParentOf(node)));
+    node = ParentOf(node);
+  }
+
+  // Update timestamp and reposition the node.
+  target = RemoveNode(target);
+  assert(ValueOf(target) == value);
+  assert(old_size == GetSize() + 1);
+  MutableTimestampOf(target) = next_timestamp_++;
+  InsertNode(target);
+  assert(old_size == GetSize());
+  return true;
+}
+
+template <typename Val>
+bool MoveToFront<Val>::ValueFromRank(size_t rank, Val* value) {
+  const size_t old_size = GetSize();
+  if (rank <= 0 || rank > old_size) {
+    return false;
+  }
+
+  uint32_t node = root_;
+  while (node) {
+    const size_t left_subtree_num_nodes = SizeOf(LeftOf(node));
+    if (rank == left_subtree_num_nodes + 1) {
+      // This is the node we are looking for.
+      node = RemoveNode(node);
+      assert(old_size == GetSize() + 1);
+      MutableTimestampOf(node) = next_timestamp_++;
+      InsertNode(node);
+      assert(old_size == GetSize());
+      *value = ValueOf(node);
+      return true;
+    }
+
+    if (rank < left_subtree_num_nodes + 1) {
+      // Descend into the left subtree. The rank is still valid.
+      node = LeftOf(node);
+    } else {
+      // Descend into the right subtree. We leave behind the left subtree and
+      // the current node, adjust the |rank| accordingly.
+      rank -= left_subtree_num_nodes + 1;
+      node = RightOf(node);
+    }
+  }
+
+  assert(0);
+  return false;
+}
+
+template <typename Val>
+void MoveToFront<Val>::InsertNode(uint32_t node) {
+  assert(!IsInTree(node));
+  assert(SizeOf(node) == 1);
+  assert(HeightOf(node) == 1);
+  assert(TimestampOf(node));
+
+  if (!root_) {
+    root_ = node;
+    return;
+  }
+
+  uint32_t iter = root_;
+  uint32_t parent = 0;
+
+  // Will determine if |node| will become the right or left child after
+  // insertion (but before balancing).
+  bool right_child;
+
+  // Find the node which will become |node|'s parent after insertion
+  // (but before balancing).
+  while (iter) {
+    parent = iter;
+    assert(TimestampOf(iter) != TimestampOf(node));
+    right_child = TimestampOf(iter) > TimestampOf(node);
+    iter = right_child ? RightOf(iter) : LeftOf(iter);
+  }
+
+  assert(parent);
+
+  // Connect node and parent.
+  MutableParentOf(node) = parent;
+  if (right_child)
+    MutableRightOf(parent) = node;
+  else
+    MutableLeftOf(parent) = node;
+
+  // Insertion is finished. Start the balancing process.
+  bool needs_rebalancing = true;
+  parent = ParentOf(node);
+
+  while (parent) {
+    UpdateNode(parent);
+
+    if (needs_rebalancing) {
+      const int parent_balance = BalanceOf(parent);
+
+      if (RightOf(parent) == node) {
+        // Added node to the right subtree.
+        if (parent_balance > 1) {
+          // Parent is right heavy, rotate left.
+          if (BalanceOf(node) < 0)
+            RotateRight(node);
+          parent = RotateLeft(parent);
+        } else if (parent_balance == 0 || parent_balance == -1) {
+          // Parent is balanced or left heavy, no need to balance further.
+          needs_rebalancing = false;
+        }
+      } else {
+        // Added node to the left subtree.
+        if (parent_balance < -1) {
+          // Parent is left heavy, rotate right.
+          if (BalanceOf(node) > 0)
+            RotateLeft(node);
+          parent = RotateRight(parent);
+        } else if (parent_balance == 0 || parent_balance == 1) {
+          // Parent is balanced or right heavy, no need to balance further.
+          needs_rebalancing = false;
+        }
+      }
+    }
+
+    assert(BalanceOf(parent) >= -1 && (BalanceOf(parent) <= 1));
+
+    node = parent;
+    parent = ParentOf(parent);
+  }
+}
+
+template <typename Val>
+uint32_t MoveToFront<Val>::RemoveNode(uint32_t node) {
+  if (LeftOf(node) && RightOf(node)) {
+    // If |node| has two children, then use another node as scapegoat and swap
+    // their contents. We pick the scapegoat on the side of the tree which has more nodes.
+    const uint32_t scapegoat = SizeOf(LeftOf(node)) >= SizeOf(RightOf(node)) ?
+        RightestDescendantOf(LeftOf(node)) : LeftestDescendantOf(RightOf(node));
+    assert(scapegoat);
+    std::swap(MutableValueOf(node), MutableValueOf(scapegoat));
+    std::swap(MutableTimestampOf(node), MutableTimestampOf(scapegoat));
+    value_to_node_[ValueOf(node)] = node;
+    value_to_node_[ValueOf(scapegoat)] = scapegoat;
+    node = scapegoat;
+  }
+
+  // |node| may have only one child at this point.
+  assert(!RightOf(node) || !LeftOf(node));
+
+  uint32_t parent = ParentOf(node);
+  uint32_t child = RightOf(node) ? RightOf(node) : LeftOf(node);
+
+  // Orphan |node| and reconnect parent and child.
+  if (child)
+    MutableParentOf(child) = parent;
+
+  if (parent) {
+    if (LeftOf(parent) == node)
+      MutableLeftOf(parent) = child;
+    else
+      MutableRightOf(parent) = child;
+  }
+
+  MutableParentOf(node) = 0;
+  MutableLeftOf(node) = 0;
+  MutableRightOf(node) = 0;
+  UpdateNode(node);
+  const uint32_t orphan = node;
+
+  if (root_ == node)
+    root_ = child;
+
+  // Removal is finished. Start the balancing process.
+  bool needs_rebalancing = true;
+  node = child;
+
+  while (parent) {
+    UpdateNode(parent);
+
+    if (needs_rebalancing) {
+      const int parent_balance = BalanceOf(parent);
+
+      if (parent_balance == 1 || parent_balance == -1) {
+        // The height of the subtree was not changed.
+        needs_rebalancing = false;
+      } else {
+        if (RightOf(parent) == node) {
+          // Removed node from the right subtree.
+          if (parent_balance < -1) {
+            // Parent is left heavy, rotate right.
+            const uint32_t sibling = LeftOf(parent);
+            if (BalanceOf(sibling) > 0)
+              RotateLeft(sibling);
+            parent = RotateRight(parent);
+          }
+        } else {
+          // Removed node from the left subtree.
+          if (parent_balance > 1) {
+            // Parent is right heavy, rotate left.
+            const uint32_t sibling = RightOf(parent);
+            if (BalanceOf(sibling) < 0)
+              RotateRight(sibling);
+            parent = RotateLeft(parent);
+          }
+        }
+      }
+    }
+
+    assert(BalanceOf(parent) >= -1 && (BalanceOf(parent) <= 1));
+
+    node = parent;
+    parent = ParentOf(parent);
+  }
+
+  return orphan;
+}
+
+template <typename Val>
+uint32_t MoveToFront<Val>::RotateLeft(const uint32_t node) {
+  const uint32_t pivot = RightOf(node);
+  assert(pivot);
+
+  // LeftOf(pivot) gets attached to node in place of pivot.
+  MutableRightOf(node) = LeftOf(pivot);
+  if (RightOf(node))
+    MutableParentOf(RightOf(node)) = node;
+
+  // Pivot gets attached to ParentOf(node) in place of node.
+  MutableParentOf(pivot) = ParentOf(node);
+  if (!ParentOf(node))
+    root_ = pivot;
+  else if (IsLeftChild(node))
+    MutableLeftOf(ParentOf(node)) = pivot;
+  else
+    MutableRightOf(ParentOf(node)) = pivot;
+
+  // Node is child of pivot.
+  MutableLeftOf(pivot) = node;
+  MutableParentOf(node) = pivot;
+
+  // Update both node and pivot. Pivot is the new parent of node, so node should
+  // be updated first.
+  UpdateNode(node);
+  UpdateNode(pivot);
+
+  return pivot;
+}
+
+template <typename Val>
+uint32_t MoveToFront<Val>::RotateRight(const uint32_t node) {
+  const uint32_t pivot = LeftOf(node);
+  assert(pivot);
+
+  // RightOf(pivot) gets attached to node in place of pivot.
+  MutableLeftOf(node) = RightOf(pivot);
+  if (LeftOf(node))
+    MutableParentOf(LeftOf(node)) = node;
+
+  // Pivot gets attached to ParentOf(node) in place of node.
+  MutableParentOf(pivot) = ParentOf(node);
+  if (!ParentOf(node))
+    root_ = pivot;
+  else if (IsLeftChild(node))
+    MutableLeftOf(ParentOf(node)) = pivot;
+  else
+    MutableRightOf(ParentOf(node)) = pivot;
+
+  // Node is child of pivot.
+  MutableRightOf(pivot) = node;
+  MutableParentOf(node) = pivot;
+
+  // Update both node and pivot. Pivot is the new parent of node, so node should
+  // be updated first.
+  UpdateNode(node);
+  UpdateNode(pivot);
+
+  return pivot;
+}
+
+template <typename Val>
+void MoveToFront<Val>::UpdateNode(uint32_t node) {
+  MutableSizeOf(node) = 1 + SizeOf(LeftOf(node)) + SizeOf(RightOf(node));
+  MutableHeightOf(node) =
+      1 + std::max(HeightOf(LeftOf(node)), HeightOf(RightOf(node)));
+}
+
+}  // namespace spvutils
+
+#endif  // LIBSPIRV_UTIL_MOVE_TO_FRONT_H_
index c26de52..199143b 100644 (file)
@@ -174,6 +174,11 @@ add_spvtools_unittest(
   SRCS huffman_codec.cpp
   LIBS ${SPIRV_TOOLS})
 
+add_spvtools_unittest(
+  TARGET move_to_front
+  SRCS move_to_front_test.cpp
+  LIBS ${SPIRV_TOOLS})
+
 add_subdirectory(opt)
 add_subdirectory(val)
 add_subdirectory(stats)
diff --git a/test/move_to_front_test.cpp b/test/move_to_front_test.cpp
new file mode 100644 (file)
index 0000000..89fc3a8
--- /dev/null
@@ -0,0 +1,785 @@
+// Copyright (c) 2017 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <algorithm>
+#include <iostream>
+#include <set>
+
+#include "gmock/gmock.h"
+#include "util/move_to_front.h"
+
+namespace {
+
+using spvutils::MoveToFront;
+
+// Class used to test the inner workings of MoveToFront.
+class MoveToFrontTester : public MoveToFront<uint32_t> {
+ public:
+  // Inserts the value in the internal tree data structure. For testing only.
+  void TestInsert(uint32_t val) {
+    InsertNode(CreateNode(val, val));
+  }
+
+  // Removes the value from the internal tree data structure. For testing only.
+  void TestRemove(uint32_t val) {
+    const auto it = value_to_node_.find(val);
+    assert(it != value_to_node_.end());
+    RemoveNode(it->second);
+  }
+
+  // Prints the internal tree data structure to |out|. For testing only.
+  void PrintTree(std::ostream& out, bool print_timestamp = false) const {
+    if (root_)
+      PrintTreeInternal(out, root_, 1, print_timestamp);
+  }
+
+  // Returns node handle corresponding to the value. The value may not be in the tree.
+  uint32_t GetNodeHandle(uint32_t value) const {
+    const auto it = value_to_node_.find(value);
+    if (it == value_to_node_.end())
+      return 0;
+
+    return it->second;
+  }
+
+  // Returns total node count (both those in the tree and removed,
+  // but not the NIL singleton).
+  size_t GetTotalNodeCount() const {
+    assert(nodes_.size());
+    return nodes_.size() - 1;
+  }
+
+ private:
+  // Prints the internal tree data structure for debug purposes in the following
+  // format:
+  // 10H3S4----5H1S1-----D2
+  //           15H2S2----12H1S1----D3
+  // Right links are horizontal, left links step down one line.
+  // 5H1S1 is read as value 5, height 1, size 1. Optionally node label can also
+  // contain timestamp (5H1S1T15). D3 stands for depth 3.
+  void PrintTreeInternal(std::ostream& out, uint32_t node, size_t depth,
+                         bool print_timestamp) const;
+};
+
+void MoveToFrontTester::PrintTreeInternal(
+    std::ostream& out, uint32_t node,
+    size_t depth, bool print_timestamp) const {
+  if (!node) {
+    out << "D" << depth - 1 << std::endl;
+    return;
+  }
+
+  const size_t kTextFieldWvaluethWithoutTimestamp = 10;
+  const size_t kTextFieldWvaluethWithTimestamp = 14;
+  const size_t text_field_wvalueth = print_timestamp ?
+      kTextFieldWvaluethWithTimestamp : kTextFieldWvaluethWithoutTimestamp;
+
+  std::stringstream label;
+  label << ValueOf(node) << "H" << HeightOf(node) << "S" << SizeOf(node);
+  if (print_timestamp)
+    label << "T" << TimestampOf(node);
+  const size_t label_length = label.str().length();
+  if (label_length < text_field_wvalueth)
+    label << std::string(text_field_wvalueth - label_length, '-');
+
+  out << label.str();
+
+  PrintTreeInternal(out, RightOf(node), depth + 1, print_timestamp);
+
+  if (LeftOf(node)) {
+    out << std::string(depth * text_field_wvalueth, ' ');
+    PrintTreeInternal(out, LeftOf(node), depth + 1, print_timestamp);
+  }
+}
+
+void CheckTree(const MoveToFrontTester& mtf, const std::string& expected,
+               bool print_timestamp = false) {
+  std::stringstream ss;
+  mtf.PrintTree(ss, print_timestamp);
+  EXPECT_EQ(expected, ss.str());
+}
+
+TEST(MoveToFront, EmptyTree) {
+  MoveToFrontTester mtf;
+  CheckTree(mtf, std::string());
+}
+
+TEST(MoveToFront, InsertLeftRotation) {
+  MoveToFrontTester mtf;
+
+  mtf.TestInsert(30);
+  mtf.TestInsert(20);
+
+  CheckTree(mtf, std::string(R"(
+30H2S2----20H1S1----D2
+)").substr(1));
+
+  mtf.TestInsert(10);
+  CheckTree(mtf, std::string(R"(
+20H2S3----10H1S1----D2
+          30H1S1----D2
+)").substr(1));
+}
+
+TEST(MoveToFront, InsertRightRotation) {
+  MoveToFrontTester mtf;
+
+  mtf.TestInsert(10);
+  mtf.TestInsert(20);
+
+  CheckTree(mtf, std::string(R"(
+10H2S2----D1
+          20H1S1----D2
+)").substr(1));
+
+  mtf.TestInsert(30);
+  CheckTree(mtf, std::string(R"(
+20H2S3----10H1S1----D2
+          30H1S1----D2
+)").substr(1));
+}
+
+TEST(MoveToFront, InsertRightLeftRotation) {
+  MoveToFrontTester mtf;
+
+  mtf.TestInsert(30);
+  mtf.TestInsert(20);
+
+  CheckTree(mtf, std::string(R"(
+30H2S2----20H1S1----D2
+)").substr(1));
+
+  mtf.TestInsert(25);
+  CheckTree(mtf, std::string(R"(
+25H2S3----20H1S1----D2
+          30H1S1----D2
+)").substr(1));
+}
+
+TEST(MoveToFront, InsertLeftRightRotation) {
+  MoveToFrontTester mtf;
+
+  mtf.TestInsert(10);
+  mtf.TestInsert(20);
+
+  CheckTree(mtf, std::string(R"(
+10H2S2----D1
+          20H1S1----D2
+)").substr(1));
+
+  mtf.TestInsert(15);
+  CheckTree(mtf, std::string(R"(
+15H2S3----10H1S1----D2
+          20H1S1----D2
+)").substr(1));
+}
+
+TEST(MoveToFront, RemoveSingleton) {
+  MoveToFrontTester mtf;
+
+  mtf.TestInsert(10);
+  CheckTree(mtf, std::string(R"(
+10H1S1----D1
+)").substr(1));
+
+  mtf.TestRemove(10);
+  CheckTree(mtf, "");
+}
+
+TEST(MoveToFront, RemoveRootWithScapegoat) {
+  MoveToFrontTester mtf;
+
+  mtf.TestInsert(10);
+  mtf.TestInsert(5);
+  mtf.TestInsert(15);
+  CheckTree(mtf, std::string(R"(
+10H2S3----5H1S1-----D2
+          15H1S1----D2
+)").substr(1));
+
+  mtf.TestRemove(10);
+  CheckTree(mtf, std::string(R"(
+15H2S2----5H1S1-----D2
+)").substr(1));
+}
+
+TEST(MoveToFront, RemoveRightRotation) {
+  MoveToFrontTester mtf;
+
+  mtf.TestInsert(10);
+  mtf.TestInsert(5);
+  mtf.TestInsert(15);
+  mtf.TestInsert(20);
+  CheckTree(mtf, std::string(R"(
+10H3S4----5H1S1-----D2
+          15H2S2----D2
+                    20H1S1----D3
+)").substr(1));
+
+  mtf.TestRemove(5);
+
+  CheckTree(mtf, std::string(R"(
+15H2S3----10H1S1----D2
+          20H1S1----D2
+)").substr(1));
+}
+
+TEST(MoveToFront, RemoveLeftRotation) {
+  MoveToFrontTester mtf;
+
+  mtf.TestInsert(10);
+  mtf.TestInsert(15);
+  mtf.TestInsert(5);
+  mtf.TestInsert(1);
+  CheckTree(mtf, std::string(R"(
+10H3S4----5H2S2-----1H1S1-----D3
+          15H1S1----D2
+)").substr(1));
+
+  mtf.TestRemove(15);
+
+  CheckTree(mtf, std::string(R"(
+5H2S3-----1H1S1-----D2
+          10H1S1----D2
+)").substr(1));
+}
+
+TEST(MoveToFront, RemoveLeftRightRotation) {
+  MoveToFrontTester mtf;
+
+  mtf.TestInsert(10);
+  mtf.TestInsert(15);
+  mtf.TestInsert(5);
+  mtf.TestInsert(12);
+  CheckTree(mtf, std::string(R"(
+10H3S4----5H1S1-----D2
+          15H2S2----12H1S1----D3
+)").substr(1));
+
+  mtf.TestRemove(5);
+
+  CheckTree(mtf, std::string(R"(
+12H2S3----10H1S1----D2
+          15H1S1----D2
+)").substr(1));
+}
+
+TEST(MoveToFront, RemoveRightLeftRotation) {
+  MoveToFrontTester mtf;
+
+  mtf.TestInsert(10);
+  mtf.TestInsert(15);
+  mtf.TestInsert(5);
+  mtf.TestInsert(8);
+  CheckTree(mtf, std::string(R"(
+10H3S4----5H2S2-----D2
+                    8H1S1-----D3
+          15H1S1----D2
+)").substr(1));
+
+  mtf.TestRemove(15);
+
+  CheckTree(mtf, std::string(R"(
+8H2S3-----5H1S1-----D2
+          10H1S1----D2
+)").substr(1));
+}
+
+TEST(MoveToFront, MultipleOperations) {
+  MoveToFrontTester mtf;
+  std::vector<uint32_t> vals =
+      { 5, 11, 12, 16, 15, 6, 14, 2, 7, 10, 4, 8, 9, 3, 1, 13 };
+
+  for (uint32_t i : vals) {
+    mtf.TestInsert(i);
+  }
+
+  CheckTree(mtf, std::string(R"(
+11H5S16---5H4S10----3H3S4-----2H2S2-----1H1S1-----D5
+                              4H1S1-----D4
+                    7H3S5-----6H1S1-----D4
+                              9H2S3-----8H1S1-----D5
+                                        10H1S1----D5
+          15H3S5----13H2S3----12H1S1----D4
+                              14H1S1----D4
+                    16H1S1----D3
+)").substr(1));
+
+  mtf.TestRemove(11);
+
+  CheckTree(mtf, std::string(R"(
+10H5S15---5H4S9-----3H3S4-----2H2S2-----1H1S1-----D5
+                              4H1S1-----D4
+                    7H3S4-----6H1S1-----D4
+                              9H2S2-----8H1S1-----D5
+          15H3S5----13H2S3----12H1S1----D4
+                              14H1S1----D4
+                    16H1S1----D3
+)").substr(1));
+
+  mtf.TestInsert(11);
+
+  CheckTree(mtf, std::string(R"(
+10H5S16---5H4S9-----3H3S4-----2H2S2-----1H1S1-----D5
+                              4H1S1-----D4
+                    7H3S4-----6H1S1-----D4
+                              9H2S2-----8H1S1-----D5
+          13H3S6----12H2S2----11H1S1----D4
+                    15H2S3----14H1S1----D4
+                              16H1S1----D4
+)").substr(1));
+
+  mtf.TestRemove(5);
+
+  CheckTree(mtf, std::string(R"(
+10H5S15---6H4S8-----3H3S4-----2H2S2-----1H1S1-----D5
+                              4H1S1-----D4
+                    8H2S3-----7H1S1-----D4
+                              9H1S1-----D4
+          13H3S6----12H2S2----11H1S1----D4
+                    15H2S3----14H1S1----D4
+                              16H1S1----D4
+)").substr(1));
+
+  mtf.TestInsert(5);
+
+  CheckTree(mtf, std::string(R"(
+10H5S16---6H4S9-----3H3S5-----2H2S2-----1H1S1-----D5
+                              4H2S2-----D4
+                                        5H1S1-----D5
+                    8H2S3-----7H1S1-----D4
+                              9H1S1-----D4
+          13H3S6----12H2S2----11H1S1----D4
+                    15H2S3----14H1S1----D4
+                              16H1S1----D4
+)").substr(1));
+
+  mtf.TestRemove(2);
+  mtf.TestRemove(1);
+  mtf.TestRemove(4);
+  mtf.TestRemove(3);
+  mtf.TestRemove(6);
+  mtf.TestRemove(5);
+  mtf.TestRemove(7);
+  mtf.TestRemove(9);
+
+  CheckTree(mtf, std::string(R"(
+13H4S8----10H3S4----8H1S1-----D3
+                    12H2S2----11H1S1----D4
+          15H2S3----14H1S1----D3
+                    16H1S1----D3
+)").substr(1));
+}
+
+TEST(MoveToFront, BiggerScaleTreeTest) {
+  MoveToFrontTester mtf;
+  std::set<uint32_t> all_vals;
+
+  const uint32_t kMagic1 = 2654435761;
+  const uint32_t kMagic2 = 10000;
+
+  for (uint32_t i = 1; i < 1000; ++i) {
+    const uint32_t val = (i * kMagic1) % kMagic2;
+    if (!all_vals.count(val)) {
+      mtf.TestInsert(val);
+      all_vals.insert(val);
+    }
+  }
+
+  for (uint32_t i = 1; i < 1000; ++i) {
+    const uint32_t val = (i * kMagic1) % kMagic2;
+    if (val % 2 == 0) {
+      mtf.TestRemove(val);
+      all_vals.erase(val);
+    }
+  }
+
+  for (uint32_t i = 1000; i < 2000; ++i) {
+    const uint32_t val = (i * kMagic1) % kMagic2;
+    if (!all_vals.count(val)) {
+      mtf.TestInsert(val);
+      all_vals.insert(val);
+    }
+  }
+
+  for (uint32_t i = 1; i < 2000; ++i) {
+    const uint32_t val = (i * kMagic1) % kMagic2;
+    if (val > 50) {
+      mtf.TestRemove(val);
+      all_vals.erase(val);
+    }
+  }
+
+  EXPECT_EQ(all_vals, std::set<uint32_t>({2, 4, 11, 13, 24, 33, 35, 37, 46}));
+
+  CheckTree(mtf, std::string(R"(
+33H4S9----11H3S5----2H2S2-----D3
+                              4H1S1-----D4
+                    13H2S2----D3
+                              24H1S1----D4
+          37H2S3----35H1S1----D3
+                    46H1S1----D3
+)").substr(1));
+}
+
+TEST(MoveToFront, RankFromValue) {
+  MoveToFrontTester mtf;
+
+  size_t rank = 0;
+  EXPECT_FALSE(mtf.RankFromValue(1, &rank));
+
+  EXPECT_TRUE(mtf.Insert(1));
+  EXPECT_TRUE(mtf.Insert(2));
+  EXPECT_TRUE(mtf.Insert(3));
+  EXPECT_FALSE(mtf.Insert(2));
+  CheckTree(mtf, std::string(R"(
+2H2S3T2-------1H1S1T1-------D2
+              3H1S1T3-------D2
+)").substr(1), /* print_timestamp = */ true);
+
+  EXPECT_FALSE(mtf.RankFromValue(4, &rank));
+
+  EXPECT_TRUE(mtf.RankFromValue(1, &rank));
+  EXPECT_EQ(3u, rank);
+
+  CheckTree(mtf, std::string(R"(
+3H2S3T3-------2H1S1T2-------D2
+              1H1S1T4-------D2
+)").substr(1), /* print_timestamp = */ true);
+
+  EXPECT_TRUE(mtf.RankFromValue(1, &rank));
+  EXPECT_EQ(1u, rank);
+
+  EXPECT_TRUE(mtf.RankFromValue(3, &rank));
+  EXPECT_EQ(2u, rank);
+
+  EXPECT_TRUE(mtf.RankFromValue(2, &rank));
+  EXPECT_EQ(3u, rank);
+
+  EXPECT_TRUE(mtf.Insert(40));
+
+  EXPECT_TRUE(mtf.RankFromValue(1, &rank));
+  EXPECT_EQ(4u, rank);
+
+  EXPECT_TRUE(mtf.Insert(50));
+
+  EXPECT_TRUE(mtf.RankFromValue(1, &rank));
+  EXPECT_EQ(2u, rank);
+
+  CheckTree(mtf, std::string(R"(
+2H3S5T7-------3H1S1T6-------D2
+              50H2S3T10-----40H1S1T8------D3
+                            1H1S1T11------D3
+)").substr(1), /* print_timestamp = */ true);
+
+  EXPECT_TRUE(mtf.RankFromValue(50, &rank));
+  EXPECT_EQ(2u, rank);
+
+  EXPECT_EQ(5u, mtf.GetSize());
+  CheckTree(mtf, std::string(R"(
+2H3S5T7-------3H1S1T6-------D2
+              1H2S3T11------40H1S1T8------D3
+                            50H1S1T12-----D3
+)").substr(1), /* print_timestamp = */ true);
+
+  EXPECT_FALSE(mtf.RankFromValue(0, &rank));
+  EXPECT_FALSE(mtf.RankFromValue(20, &rank));
+}
+
+TEST(MoveToFront, ValueFromRank) {
+  MoveToFrontTester mtf;
+
+  uint32_t value = 0;
+  EXPECT_FALSE(mtf.ValueFromRank(0, &value));
+  EXPECT_FALSE(mtf.ValueFromRank(1, &value));
+
+  EXPECT_TRUE(mtf.Insert(1));
+  EXPECT_TRUE(mtf.Insert(2));
+  EXPECT_TRUE(mtf.Insert(3));
+
+  EXPECT_TRUE(mtf.ValueFromRank(3, &value));
+  EXPECT_EQ(1u, value);
+
+  EXPECT_TRUE(mtf.ValueFromRank(1, &value));
+  EXPECT_EQ(1u, value);
+
+  EXPECT_TRUE(mtf.ValueFromRank(2, &value));
+  EXPECT_EQ(3u, value);
+
+  EXPECT_EQ(3u, mtf.GetSize());
+
+  CheckTree(mtf, std::string(R"(
+1H2S3T5-------2H1S1T2-------D2
+              3H1S1T6-------D2
+)").substr(1), /* print_timestamp = */ true);
+
+  EXPECT_TRUE(mtf.ValueFromRank(3, &value));
+  EXPECT_EQ(2u, value);
+
+  CheckTree(mtf, std::string(R"(
+3H2S3T6-------1H1S1T5-------D2
+              2H1S1T7-------D2
+)").substr(1), /* print_timestamp = */ true);
+
+  EXPECT_TRUE(mtf.Insert(10));
+  CheckTree(mtf, std::string(R"(
+3H3S4T6-------1H1S1T5-------D2
+              2H2S2T7-------D2
+                            10H1S1T8------D3
+)").substr(1), /* print_timestamp = */ true);
+
+  EXPECT_TRUE(mtf.ValueFromRank(1, &value));
+  EXPECT_EQ(10u, value);
+}
+
+TEST(MoveToFront, Remove) {
+  MoveToFrontTester mtf;
+
+  EXPECT_FALSE(mtf.Remove(1));
+  EXPECT_EQ(0u, mtf.GetTotalNodeCount());
+
+  EXPECT_TRUE(mtf.Insert(1));
+  EXPECT_TRUE(mtf.Insert(2));
+  EXPECT_TRUE(mtf.Insert(3));
+
+  CheckTree(mtf, std::string(R"(
+2H2S3T2-------1H1S1T1-------D2
+              3H1S1T3-------D2
+)").substr(1), /* print_timestamp = */ true);
+
+  EXPECT_EQ(1u, mtf.GetNodeHandle(1));
+  EXPECT_EQ(3u, mtf.GetTotalNodeCount());
+  EXPECT_TRUE(mtf.Remove(1));
+  EXPECT_EQ(3u, mtf.GetTotalNodeCount());
+
+  CheckTree(mtf, std::string(R"(
+2H2S2T2-------D1
+              3H1S1T3-------D2
+)").substr(1), /* print_timestamp = */ true);
+
+  uint32_t value = 0;
+  EXPECT_TRUE(mtf.ValueFromRank(2, &value));
+  EXPECT_EQ(2u, value);
+
+  CheckTree(mtf, std::string(R"(
+3H2S2T3-------D1
+              2H1S1T4-------D2
+)").substr(1), /* print_timestamp = */ true);
+
+  EXPECT_TRUE(mtf.Insert(1));
+  EXPECT_EQ(1u, mtf.GetNodeHandle(1));
+  EXPECT_EQ(3u, mtf.GetTotalNodeCount());
+}
+
+TEST(MoveToFront, LargerScale) {
+  MoveToFrontTester mtf;
+  uint32_t value = 0;
+  size_t rank = 0;
+
+  for (uint32_t i = 1; i < 1000; ++i) {
+    ASSERT_TRUE(mtf.Insert(i));
+    ASSERT_EQ(i, mtf.GetSize());
+
+    ASSERT_TRUE(mtf.RankFromValue(i, &rank));
+    ASSERT_EQ(1u, rank);
+
+    ASSERT_TRUE(mtf.ValueFromRank(1, &value));
+    ASSERT_EQ(i, value);
+  }
+
+  ASSERT_TRUE(mtf.ValueFromRank(999, &value));
+  ASSERT_EQ(1u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(999, &value));
+  ASSERT_EQ(2u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(999, &value));
+  ASSERT_EQ(3u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(999, &value));
+  ASSERT_EQ(4u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(999, &value));
+  ASSERT_EQ(5u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(999, &value));
+  ASSERT_EQ(6u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(101, &value));
+  ASSERT_EQ(905u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(101, &value));
+  ASSERT_EQ(906u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(101, &value));
+  ASSERT_EQ(907u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(201, &value));
+  ASSERT_EQ(805u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(201, &value));
+  ASSERT_EQ(806u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(201, &value));
+  ASSERT_EQ(807u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(301, &value));
+  ASSERT_EQ(705u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(301, &value));
+  ASSERT_EQ(706u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(301, &value));
+  ASSERT_EQ(707u, value);
+
+  ASSERT_TRUE(mtf.RankFromValue(605, &rank));
+  ASSERT_EQ(401u, rank);
+
+  ASSERT_TRUE(mtf.RankFromValue(606, &rank));
+  ASSERT_EQ(401u, rank);
+
+  ASSERT_TRUE(mtf.RankFromValue(607, &rank));
+  ASSERT_EQ(401u, rank);
+
+  ASSERT_TRUE(mtf.ValueFromRank(1, &value));
+  ASSERT_EQ(607u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(2, &value));
+  ASSERT_EQ(606u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(3, &value));
+  ASSERT_EQ(605u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(4, &value));
+  ASSERT_EQ(707u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(5, &value));
+  ASSERT_EQ(706u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(6, &value));
+  ASSERT_EQ(705u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(7, &value));
+  ASSERT_EQ(807u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(8, &value));
+  ASSERT_EQ(806u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(9, &value));
+  ASSERT_EQ(805u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(10, &value));
+  ASSERT_EQ(907u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(11, &value));
+  ASSERT_EQ(906u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(12, &value));
+  ASSERT_EQ(905u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(13, &value));
+  ASSERT_EQ(6u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(14, &value));
+  ASSERT_EQ(5u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(15, &value));
+  ASSERT_EQ(4u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(16, &value));
+  ASSERT_EQ(3u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(17, &value));
+  ASSERT_EQ(2u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(18, &value));
+  ASSERT_EQ(1u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(19, &value));
+  ASSERT_EQ(999u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(20, &value));
+  ASSERT_EQ(998u, value);
+
+  ASSERT_TRUE(mtf.ValueFromRank(21, &value));
+  ASSERT_EQ(997u, value);
+
+  ASSERT_TRUE(mtf.RankFromValue(997, &rank));
+  ASSERT_EQ(1u, rank);
+
+  ASSERT_TRUE(mtf.RankFromValue(998, &rank));
+  ASSERT_EQ(2u, rank);
+
+  ASSERT_TRUE(mtf.RankFromValue(996, &rank));
+  ASSERT_EQ(22u, rank);
+
+  ASSERT_TRUE(mtf.Remove(995));
+
+  ASSERT_TRUE(mtf.RankFromValue(994, &rank));
+  ASSERT_EQ(23u, rank);
+
+  for (uint32_t i = 10; i < 1000; ++i) {
+    if (i != 995) {
+      ASSERT_TRUE(mtf.Remove(i));
+    } else {
+      ASSERT_FALSE(mtf.Remove(i));
+    }
+  }
+
+  CheckTree(mtf, std::string(R"(
+6H4S9T3028----8H2S3T24------7H1S1T21------D3
+                            9H1S1T27------D3
+              2H3S5T3032----4H2S3T3030----5H1S1T3029----D4
+                                          3H1S1T3031----D4
+                            1H1S1T3033----D3
+)").substr(1), /* print_timestamp = */ true);
+
+  ASSERT_TRUE(mtf.Insert(1000));
+  ASSERT_TRUE(mtf.ValueFromRank(1, &value));
+  ASSERT_EQ(1000u, value);
+}
+
+TEST(MoveToFront, String) {
+  MoveToFront<std::string> mtf;
+
+  EXPECT_TRUE(mtf.Insert("AAA"));
+  EXPECT_TRUE(mtf.Insert("BBB"));
+  EXPECT_TRUE(mtf.Insert("CCC"));
+  EXPECT_FALSE(mtf.Insert("AAA"));
+
+  std::string value;
+  EXPECT_TRUE(mtf.ValueFromRank(2, &value));
+  EXPECT_EQ("BBB", value);
+
+  EXPECT_TRUE(mtf.ValueFromRank(2, &value));
+  EXPECT_EQ("CCC", value);
+
+  size_t rank = 0;
+  EXPECT_TRUE(mtf.RankFromValue("AAA", &rank));
+  EXPECT_EQ(3u, rank);
+
+  EXPECT_FALSE(mtf.ValueFromRank(0, &value));
+  EXPECT_FALSE(mtf.RankFromValue("ABC", &rank));
+  EXPECT_FALSE(mtf.Remove("ABC"));
+
+  EXPECT_TRUE(mtf.Remove("AAA"));
+  EXPECT_FALSE(mtf.Remove("AAA"));
+  EXPECT_FALSE(mtf.RankFromValue("AAA", &rank));
+
+  EXPECT_TRUE(mtf.Insert("AAA"));
+  EXPECT_TRUE(mtf.RankFromValue("AAA", &rank));
+  EXPECT_EQ(1u, rank);
+}
+
+}  // anonymous namespace