5

I'm working through some problems on leetcode to get better at using Rust during interviews. As a first attempt to solve this problem, I thought of representing triplet solutions a + b + c = 0 by storing a, b, and c in a solution: HashSet<i32>, then storing that solution: HashSet<i32> in another collection solution_set: HashSet<HashSet<i32>>. Crazy, right?

The exercise explicitly states that redundant triplets don't qualify, so rather than storing the triplets in solution: Vec<i32>s where order might change a Vec's hash value, I thought I'd store the triplets in solution: HashSet<i32> so any ordering of a, b, and c resolves to the same solution. Additionally, it would be O(1) time to verify that a triplet already exists in solution_set: HashSet<HashSet<i32>>, rather than O(n) to check if it exists in the alternative solution_set: Vec<HashSet<i32>>. Finally, I know the return value is Vec<Vec<i32>>, but that's solved by drain()ing the solution: HashSet<i32> into Vec<i32>, and then draining the resulting Iter<Vec<i32>> into a Vec<Vec<i32>>.

I recognize that HashSet<T> does not implement Hash, so I decided to try to myself, and now I'm up a paddle without a creek. I looked here to learn about implementing Hash for a struct, and here to learn how to implement a trait on a struct I don't own, but now I'm re-implementing all the functions handles I need from HashSet (new(), drain(), insert(), etc) on HashSetWrapper. The compiler is also compaining about other traits too like PartialEq, so I've really opened pandora's box on this one. I just feel like this isn't the most "Rusty" way to do this.

Also, I know that implementing hashes correctly is not trivial, and as this is an effort in best practices I'd like some help figuring out the most "Rusty" way to implement my solution is. I haven't actually gotten it to work yet, but here's the code I have so far:

use std::collections::HashSet;
use std::hash::{Hash, Hasher};

#[derive(PartialEq)]
struct HashSetWrapper<T>(HashSet<T>);

impl<T: Hash> HashSetWrapper<T> {
    fn new() -> Self {
        HashSetWrapper(HashSet::<T>::new())
    }

    fn insert(&self, value: T) {
        self.0.insert(value);
    }
}

impl<T: Hash> Hash for HashSetWrapper<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        for value in &self.0 {
            value.hash(state);
        }
    }
}

impl Solution {
    pub fn three_sum(nums: Vec<i32>) -> Vec<Vec<i32>> {

        let mut solution_set: HashSetWrapper<HashSet<i32>> = HashSetWrapper::new();

        for (i, a) in nums[0..(nums.len() - 2)].iter().enumerate() {
            for (j, b) in nums[i..(nums.len() - 1)].iter().enumerate() {
                for c in nums[j..].iter() {
                    if a + b + c == 0 { 
                        let mut temp = HashSet::<i32>::new();
                        temp.insert(*a);
                        temp.insert(*b);
                        temp.insert(*c);
                        solution_set.insert(temp); }
                }
            }
        }
        solution_set.drain().map(|inner_set| inner_set.drain().collect::<Vec<_>>()).collect::<Vec<_>>()
    }
}

I still need to implement a drain() for my wrapper class, but I'm not even sure I'm going in the right direction. How would you solve this problem? How would you implement Hash on HashSet? I'd love to know!

Below are the errors the compiler is giving me:

Line 5, Char 26: binary operation `==` cannot be applied to type `std::collections::HashSet<T>` (solution.rs)
  |
5 | struct HashSetWrapper<T>(HashSet<T>);
  |                          ^^^^^^^^^^
  |
  = note: an implementation of `std::cmp::PartialEq` might be missing for `std::collections::HashSet<T>`
Line 5, Char 26: binary operation `!=` cannot be applied to type `std::collections::HashSet<T>` (solution.rs)
  |
5 | struct HashSetWrapper<T>(HashSet<T>);
  |                          ^^^^^^^^^^
  |
  = note: an implementation of `std::cmp::PartialEq` might be missing for `std::collections::HashSet<T>`
Line 9, Char 38: no function or associated item named `new` found for type `std::collections::HashSet<T>` in the current scope (solution.rs)
   |
9 |         HashSetWrapper(HashSet::<T>::new())
   |                                      ^^^ function or associated item not found in `std::collections::HashSet<T>`
   |
   = note: the method `new` exists but the following trait bounds were not satisfied:
           `T : std::cmp::Eq`
Line 13, Char 16: no method named `insert` found for type `std::collections::HashSet<T>` in the current scope (solution.rs)
   |
13 |         self.0.insert(value);
   |                ^^^^^^ method not found in `std::collections::HashSet<T>`
   |
   = note: the method `insert` exists but the following trait bounds were not satisfied:
           `T : std::cmp::Eq`
Line 28, Char 62: the trait bound `std::collections::HashSet<i32>: std::hash::Hash` is not satisfied (solution.rs)
   |
8  |     fn new() -> Self {
   |     ---------------- required by `HashSetWrapper::<T>::new`
...
28 |         let mut solution_set: HashSetWrapper<HashSet<i32>> = HashSetWrapper::new();
   |                                                              ^^^^^^^^^^^^^^^^^^^ the trait `std::hash::Hash` is not implemented for `std::collections::HashSet<i32>`
Line 38, Char 38: no method named `insert` found for type `HashSetWrapper<std::collections::HashSet<i32>>` in the current scope (solution.rs)
   |
5  | struct HashSetWrapper<T>(HashSet<T>);
   | ------------------------------------- method `insert` not found for this
...
38 |                         solution_set.insert(temp); }
   |                                      ^^^^^^ method not found in `HashSetWrapper<std::collections::HashSet<i32>>`
   |
   = note: the method `insert` exists but the following trait bounds were not satisfied:
           `std::collections::HashSet<i32> : std::hash::Hash`
Line 42, Char 22: no method named `drain` found for type `HashSetWrapper<std::collections::HashSet<i32>>` in the current scope (solution.rs)
   |
5  | struct HashSetWrapper<T>(HashSet<T>);
   | ------------------------------------- method `drain` not found for this
...
42 |         solution_set.drain().map(|inner_set| inner_set.drain().collect::<Vec<_>>()).collect::<Vec<_>>()
   |                      ^^^^^ method not found in `HashSetWrapper<std::collections::HashSet<i32>>`
error: aborting due to 7 previous errors
  • I would either store the triplets as sorted tuples `(a, b, c)` or in a simple struct `Triplet (a, b, c)` and implement `Hash` as a `xor` of the three hashes (to get order independence). – Jmb Apr 22 '20 at 06:59
  • 3
    BTW your current `Hash` implementation is dependent on the order of iteration through the `HashSet`, which is [arbitrary](https://doc.rust-lang.org/std/collections/struct.HashSet.html#method.iter) and so not guaranteed to stay the same even for two calls on the same `HashSet` ! – Jmb Apr 22 '20 at 07:02
  • 1
    @Jmb Unfortunately, it's not easily possible to xor the subhashes. This would require to construct a new `Hasher` of type `H` inside the `hash()` method, and there is no good way to do that. A [mediocre solution for the problem is included in this answer](https://stackoverflow.com/a/60884343). – Sven Marnach Apr 22 '20 at 08:45
  • 1
    Regarding the actual problem on Leetcode, you may be overthinking the deduplication a bit. Just sort the input array before starting, so all your triples will come out sorted, A triple of `i32` implements `Hash` by default, so you can stick them into a `HashSet`, and since the triples are sorted this will eliminate duplicates. – Sven Marnach Apr 22 '20 at 09:32
  • @SvenMarnach yes, that is the answer I was thinking of when I talked of `xor`ing the sub-hashes. For this question, personally, I would go for the "sorted tuples" solution. – Jmb Apr 22 '20 at 09:33
  • Note that your current strategy won't produce a correct result anyways (e.g., [-1, 0, 1, 2, -1, -4] as input would return [[1, -1, 0], [-1, 2]] instead of [[-1, 0, 1], [-1, -1, 2]] since the set doesn't take into account duplicate numbers in the input. In addition, your `if a + b + c == 0` probably needs an i != j, i != k, etc. – zgerd Apr 24 '20 at 02:30
  • Yep, I noticed that after I came back to the problem. I ended up doing something similar to the suggested answer by storing sorted `Vec` in a `HashSet>`. All of you are correct that this was way more complicated than it needed to be, but I was interested in following the rabbit hole I found in defining traits and such.Thank you all for your help! –  May 02 '20 at 21:17

1 Answers1

-1

I've just gone through your code and people's comments. I think that you're overly complicated yourself with the HashSet<i32>, and then having to implement all of the trait function for your HashSetWrapper. A simpler version is just to have a simple struct to keep your triplet, and let it derives from Hash, Eq and PartialEq using macro. To make the de-dup work automagically, we can sort the triplet as an earlier comment.

Following is my code that still roughly follow the logic of your three_sum implementation (it has a bug, btw), with this suggestion.

#[derive(Hash, Eq, PartialEq, Debug)]
pub struct Triplet {
    x: i32,
    y: i32,
    z: i32,
}

impl Triplet {
    pub fn new(x: i32, y: i32, z: i32) -> Triplet {
        let mut v = vec![x, y, z];
        v.sort();
        Triplet {
            x: v[0],
            y: v[1],
            z: v[2],
        }
    }

    pub fn to_vec(&self) -> Vec<i32> {
        vec![self.x, self.y, self.z]
    }
}

pub fn three_sum(nums: Vec<i32>) -> Vec<Vec<i32>> {
    let mut res: HashSet<Triplet> = HashSet::new();
    for (i, a) in nums[0..(nums.len() - 2)].iter().enumerate() {
        for (j, b) in nums[i+1..(nums.len() - 1)].iter().enumerate() {
            for c in nums[j+1..].iter() {
                if a + b + c == 0 {
                    let triplet = Triplet::new(*a, *b, *c);
                    res.insert(triplet);
                }
            }
        }
    }
    res.into_iter().map(|t| t.to_vec()).collect()
}

Test code:

    #[test]
    fn test_three_sum() {
        let result = vec![vec![-1, -1, 2], vec![-1, 0, 1]];
        assert_eq!(three_sum(vec![-1, 0, 1, 2, -1, -4]), result)
    }

Result:

running 1 test
test tests::test_three_sum ... ok
Dat Nguyen
  • 1,626
  • 22
  • 25
  • This is a great solution! I went back and tried this again, and using `HashSet` doesn't solve the problem anyway. if you tried to store the triplet `(-1, -1, 2)` it colapses to `(-1, 2)`, and thus is no longer a triplet. –  May 02 '20 at 21:14