LeetCode 문제를 풀던 와중에 TreeSet을 사용하게 되었고, TreeSet의 요소를 지우는 remove 메서드도 사용했다.
일부 테스트케이스에서 계속해서 정답이 틀리다고 나오는 것이었다.
해당 문제를 풀기위해서 조금 더 디버깅을 해보니 내가 원하는 요소가 제대로 remove 되지 않는 이슈가 있었다.
문제에 대해서 간단하게 설명하면, 요리는 각각의 점수를 가지고 있고, 특정 메서드를 호출하면 최고 요리의 이름을 가져오는 메서드를 설계하면 되는 문제이다.
Food
Class를 정의한다. (문제에서 Food
의 이름은 고유하다고 했기 때문에 이름만 같으면 동일하다고 판단하기 위해서 equals
와 hashCode
를 override
했다.@Getter
class Food {
final String name;
final int rating;
public Food(final String name, final int rating) {
this.name = name;
this.rating = rating;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Food food = (Food) o;
return Objects.equals(getName(), food.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName());
}
}
TreeSet
은 Food
라는 객체를 가지고, Food
를 정렬하기 위해서 FOOD_COMPARATOR
를 사용한다.// Raiting에 따라 우선 정렬을 하고 동일한 점수일 경우 이름으로 정렬을 한다.
final Comparator<Food> FOOD_COMPARATOR = Comparator.comparingInt(Food::getRating).reversed().thenComparing(Food::getName);
final TreeSet<Food> set = new TreeSet<>(FOOD_COMPARATOR)
set
에서 특정 객체를 지우기 위해서 Food
를 생성 후 해당 Food
를 기반으로 삭제를 한다.set.remove(new Food(foodName, 0));
new Food(foodName, 10)
과 new Food(foodName, 0)
은 equals
가 true
이기 때문에, set
에 new Food(foodName, 10)
이 들어가 있으면 set.remove(new Food(foodName, 0))
메서드로 new Food(foodName, 10)
객체의 삭제가 가능하다고 생각을 했다.final TreeSet<Food> set = new TreeSet<>(FOOD_COMPARATOR)
set.add(new Food(foodName, 10));
boolean result = set.remove(new Food(foodName, 0));
//내 예상
result == true
//실제 결과
result == false
remove
를 하지 못하는 것이었다.TreeSet
의 remove
코드를 확인하기로 했다.Comparotor
를 넣어서 TreeSet
을 생성하면 TreeMap
을 사용해서 TreeSet
이 만들어진다.remove
메서드는 TreeMap
에게 삭제 로직을 위임한다.//TreeSet
private transient NavigableMap<E,Object> m;
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
TreeMap
에서는 일단 넘어온 key
의 해당 Entry
를 찾고 해당 Entry
를 찾으면 삭제 로직을 진행한다.//TreeMap
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
그래서 나는 해당 메서드의 순서대로 getEntry
로직을 우선 살펴봤고 getEntry
메서드를 확인하니 왜 내 예상과 다르게 해당 Food
가 삭제가 안되었는지 알 수 있었다.
코드를 살펴보면 root
에서 부터 while
을 돌면서 Comparator
를 사용해서 compareTo
메서드를 통해 key
에 해당하는 Entry
를 찾는 로직이 들어 있다.
(TreeMap
같은 경우 RedBlackTree
로 구현이 되어있다.)
여기서 Comparator
의 비교 값을 고려하지 않은채 new Food(foodName, 0)
와 같이 넣게 되면 compareTo
메서드에서 전혀 다른 비교값이 나오게 된다.
그리고 내가 원하는 객체를 제대로 찾아가지 못하기 때문에 해당 Entry
를 찾지 못하고 remov
e 메서드의 return
으로 false
가 나오는 것이었다.
//TreeMap
final TreeMap.Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
TreeMap.Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
자료 구조의 특성에 대해서는 이미 어느정도 알고 있다라고 생각하고 있었는데, 실제로 이슈로 마주치니 "나는 겉핥기로만 알고 있었다"라는 사실을 다시 한번 더 깨닫게 되었다.
머리로만 알고 있던 자료구조를 다시 한번 더 생각해보는 좋은 계기였다.