From 54a48d2dd1647befc760ab05d807639f79375d97 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 27 Feb 2021 11:36:53 -0800 Subject: [PATCH] Add move_index to change the position of an entry This moves the position of a key-value pair from one index to another by shifting all other pairs in-between, making this an O(n) operation. This could be used as a building-block for other operations, like #173 which wants to insert at a particular index. You can `insert_full` to insert it _somewhere_, then choose whether to `move_index` depending on whether you want to also change pre-existing entries. --- src/map.rs | 13 +++++++++ src/map/core.rs | 72 ++++++++++++++++++++++++++++++++++++++++--------- src/set.rs | 13 +++++++++ tests/quick.rs | 39 +++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 12 deletions(-) diff --git a/src/map.rs b/src/map.rs index 93622bc6..a1647d25 100644 --- a/src/map.rs +++ b/src/map.rs @@ -876,6 +876,19 @@ impl IndexMap { self.core.shift_remove_index(index) } + /// Moves the position of a key-value pair from one index to another + /// by shifting all other pairs in-between. + /// + /// * If `from < to`, the other pairs will shift down while the targeted pair moves up. + /// * If `from > to`, the other pairs will shift up while the targeted pair moves down. + /// + /// ***Panics*** if `from` or `to` are out of bounds. + /// + /// Computes in **O(n)** time (average). + pub fn move_index(&mut self, from: usize, to: usize) { + self.core.move_index(from, to) + } + /// Swaps the position of two key-value pairs in the map. /// /// ***Panics*** if `a` or `b` are out of bounds. diff --git a/src/map/core.rs b/src/map/core.rs index 0bc54621..fca9c384 100644 --- a/src/map/core.rs +++ b/src/map/core.rs @@ -283,29 +283,77 @@ impl IndexMapCore { /// /// The index should already be removed from `self.indices`. fn shift_remove_finish(&mut self, index: usize) -> (K, V) { - // use Vec::remove, but then we need to update the indices that point - // to all of the other entries that have to move + // Correct indices that point to the entries that followed the removed entry. + self.decrement_indices(index + 1, self.entries.len()); + + // Use Vec::remove to actually remove the entry. let entry = self.entries.remove(index); + (entry.key, entry.value) + } - // correct indices that point to the entries that followed the removed entry. - // use a heuristic between a full sweep vs. a `find()` for every shifted item. - let raw_capacity = self.indices.buckets(); - let shifted_entries = &self.entries[index..]; - if shifted_entries.len() > raw_capacity / 2 { - // shift all indices greater than `index` + /// Decrement all indices in the range `start..end`. + /// + /// The index `start - 1` should not exist in `self.indices`. + /// All entries should still be in their original positions. + fn decrement_indices(&mut self, start: usize, end: usize) { + // Use a heuristic between a full sweep vs. a `find()` for every shifted item. + let shifted_entries = &self.entries[start..end]; + if shifted_entries.len() > self.indices.buckets() / 2 { + // Shift all indices in range. for i in self.indices_mut() { - if *i > index { + if start <= *i && *i < end { *i -= 1; } } } else { - // find each following entry to shift its index - for (i, entry) in (index + 1..).zip(shifted_entries) { + // Find each entry in range to shift its index. + for (i, entry) in (start..end).zip(shifted_entries) { update_index(&mut self.indices, entry.hash, i, i - 1); } } + } - (entry.key, entry.value) + /// Increment all indices in the range `start..end`. + /// + /// The index `end` should not exist in `self.indices`. + /// All entries should still be in their original positions. + fn increment_indices(&mut self, start: usize, end: usize) { + // Use a heuristic between a full sweep vs. a `find()` for every shifted item. + let shifted_entries = &self.entries[start..end]; + if shifted_entries.len() > self.indices.buckets() / 2 { + // Shift all indices in range. + for i in self.indices_mut() { + if start <= *i && *i < end { + *i += 1; + } + } + } else { + // Find each entry in range to shift its index, updated in reverse so + // we never have duplicated indices that might have a hash collision. + for (i, entry) in (start..end).zip(shifted_entries).rev() { + update_index(&mut self.indices, entry.hash, i, i + 1); + } + } + } + + pub(super) fn move_index(&mut self, from: usize, to: usize) { + let from_hash = self.entries[from].hash; + if from != to { + // Use a sentinal index so other indices don't collide. + update_index(&mut self.indices, from_hash, from, usize::MAX); + + // Update all other indices and rotate the entry positions. + if from < to { + self.decrement_indices(from + 1, to + 1); + self.entries[from..=to].rotate_left(1); + } else if to < from { + self.increment_indices(to, from); + self.entries[to..=from].rotate_right(1); + } + + // Change the sentinal index to its final position. + update_index(&mut self.indices, from_hash, usize::MAX, to); + } } /// Remove an entry by swapping it with the last diff --git a/src/set.rs b/src/set.rs index bdcbaffb..60c4ebdb 100644 --- a/src/set.rs +++ b/src/set.rs @@ -731,6 +731,19 @@ impl IndexSet { self.map.shift_remove_index(index).map(|(x, ())| x) } + /// Moves the position of a value from one index to another + /// by shifting all other values in-between. + /// + /// * If `from < to`, the other values will shift down while the targeted value moves up. + /// * If `from > to`, the other values will shift up while the targeted value moves down. + /// + /// ***Panics*** if `from` or `to` are out of bounds. + /// + /// Computes in **O(n)** time (average). + pub fn move_index(&mut self, from: usize, to: usize) { + self.map.move_index(from, to) + } + /// Swaps the position of two values in the set. /// /// ***Panics*** if `a` or `b` are out of bounds. diff --git a/tests/quick.rs b/tests/quick.rs index 4f462878..792aeb7a 100644 --- a/tests/quick.rs +++ b/tests/quick.rs @@ -216,6 +216,45 @@ quickcheck_limit! { map[&key] == value && map[i] == value }) } + + fn swap_indices(vec: Vec, a: usize, b: usize) -> TestResult { + let mut set = IndexSet::::from_iter(vec); + if a >= set.len() || b >= set.len() { + return TestResult::discard(); + } + + let mut vec = Vec::from_iter(set.iter().cloned()); + vec.swap(a, b); + + set.swap_indices(a, b); + + // Check both iteration order and hash lookups + assert!(set.iter().eq(vec.iter())); + assert!(vec.iter().enumerate().all(|(i, x)| { + set.get_index_of(x) == Some(i) + })); + TestResult::passed() + } + + fn move_index(vec: Vec, from: usize, to: usize) -> TestResult { + let mut set = IndexSet::::from_iter(vec); + if from >= set.len() || to >= set.len() { + return TestResult::discard(); + } + + let mut vec = Vec::from_iter(set.iter().cloned()); + let x = vec.remove(from); + vec.insert(to, x); + + set.move_index(from, to); + + // Check both iteration order and hash lookups + assert!(set.iter().eq(vec.iter())); + assert!(vec.iter().enumerate().all(|(i, x)| { + set.get_index_of(x) == Some(i) + })); + TestResult::passed() + } } use crate::Op::*;