문제 조건
배열이 중복되면 안됨 -> 그럼 hashset을 쓸까?
list 내의 원소들이 순서를 바꾸어도 같은 객체로 인식해야하기 때문에 정렬을 해서 넣어주도록 했다.
import java.util.*;
import java.util.stream.Collectors;
public class Solution {
public static List<List<Integer>> fourSum(int[] nums, int target) {
int length = nums.length;
HashSet<List<Integer>> answer = new HashSet<List<Integer>>();
for (int i = 0; i < length; i++) {
for (int j = i + 1; j < length; j++) {
if (i == j) {
continue;
}
for (int k = j + 1; k < length; k++) {
if (i == k || j == k) {
continue;
}
for (int l = k + 1; l < length; l++) {
if (i == l || j == l || k == l) {
continue;
}
if (nums[i] + nums[j] + nums[k] + nums[l] == target) {
List<Integer> subAnswer = Arrays.asList(nums[i], nums[j], nums[k], nums[l]);
Collections.sort(subAnswer);
answer.add(subAnswer);
}
}
}
}
}
System.out.println(answer.size());
return answer.stream().collect(Collectors.toList());
}
}
최적화
투 포인터를 이용해서 O(N^4)를 줄여보자
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
return kSum(nums, target, 0, 4);
}
public List<List<Integer>> kSum(int[] nums, int target, int start, int k) {
List<List<Integer>> res = new ArrayList<>();
if (start == nums.length || nums[start] * k > target || target > nums[nums.length - 1] * k)
return res;
if (k == 2)
return twoSum(nums, target, start);
for (int i = start; i < nums.length; ++i)
if (i == start || nums[i - 1] != nums[i])
for (var set : kSum(nums, target - nums[i], i + 1, k - 1)) {
res.add(new ArrayList<>(Arrays.asList(nums[i])));
res.get(res.size() - 1).addAll(set);
}
return res;
}
public List<List<Integer>> twoSum(int[] nums, int target, int start) {
List<List<Integer>> res = new ArrayList<>();
int lo = start, hi = nums.length - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
if (sum < target || (lo > start && nums[lo] == nums[lo - 1]))
++lo;
else if (sum > target || (hi < nums.length - 1 && nums[hi] == nums[hi + 1]))
--hi;
else
res.add(Arrays.asList(nums[lo++], nums[hi--]));
}
return res;
}
배운점
비슷하게 hashset안에 int[]를 넣어서 해봤을 때에는 왜 틀릴까?
import java.util.*;
import java.util.stream.Collectors;
public class Solution {
public static List<List<Integer>> fourSum(int[] nums, int target) {
int length = nums.length;
HashSet<int[]> answer = new HashSet<int[]>();
for (int i = 0; i < length; i++) {
for (int j = i + 1; j < length; j++) {
if (i == j) {
continue;
}
for (int k = j + 1; k < length; k++) {
if (i == k || j == k) {
continue;
}
for (int l = k + 1; l < length; l++) {
if (i == l || j == l || k == l) {
continue;
}
if (nums[i] + nums[j] + nums[k] + nums[l] == target) {
int[] subAnswer = new int[4];
subAnswer[0] = nums[i];
subAnswer[1] = nums[j];
subAnswer[2] = nums[k];
subAnswer[3] = nums[l];
Arrays.sort(subAnswer);
answer.add(subAnswer);
}
}
}
}
}
return answer.stream()
.map(i -> Arrays.stream(i).boxed().collect(Collectors.toList()))
.collect(Collectors.toList());
}
}
결과 - HashSet이 같은 배열임을 인식하지 못한다. (지금 생각해보면 당연한 소리임...)
일단 hashSet 클래스를 보면 결국 HashMap을 이용해서 push하는 것과 같다.
private transient HashMap<E,Object> map;
/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element {@code e} to this set if
* this set contains no element {@code e2} such that
* {@code Objects.equals(e, e2)}.
* If this set already contains the element, the call leaves the set
* unchanged and returns {@code false}.
*
* @param e element to be added to this set
* @return {@code true} if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashMap의 put 함수는 아래와 같이 구현되는데, 같은 객체인지 판단할 때 hashCode를 사용함을 알 수 있다.
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}.
* (A {@code null} return can also indicate that the map
* previously associated {@code null} with {@code key}.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
ArrayList 컬렉션에서는 이 hashCode를 오버라이드 하고 있었기 때문에,
오버라이드 된 해시코드를 사용하여 값이 같으면 동일 객체임을 판별할 수 있었던 것이다.
/**
* {@inheritDoc}
*/
public int hashCode() {
int expectedModCount = modCount;
int hash = hashCodeRange(0, size);
checkForComodification(expectedModCount);
return hash;
}
ArrayList에서는 해시코드를 entity의 해시코드의 합으로 정의하고 있다.
즉, ArrayList<Integer>의 해시코드는 Integer 의 해시코드들을 사용하여 만드는데, Integer에서 정의된 해시코드의 경우
값이 같은 경우 같은 해시코드를 갖게 된다.
/**
* Returns a hash code for an {@code int} value; compatible with
* {@code Integer.hashCode()}.
*
* @param value the value to hash
* @since 1.8
*
* @return a hash code value for an {@code int} value.
*/
public static int hashCode(int value) {
return value;
}
결론적으로, ArrayList<Integer>의 경우 순서가 동일하고, 같은 값을 가진 리스트의 경우 같은 해시코드 값을 갖게 되어 hashSet에 들어갈 때 중복제거가 가능하다.
이와 달리 int[]의 경우 hashCode가 정의되어 있지 않기 때문에 부모 클래스, 즉 Node에 정의되어 있는 hashCode를 사용하게 된다.
'알고리즘 공부 > leetcode' 카테고리의 다른 글
leetcode: Trapping Rain Water (0) | 2020.12.27 |
---|---|
leetcode: Median of two sorted Arrays (0) | 2020.12.26 |
leetcode: Letter Combinations of a Phone number (0) | 2020.12.26 |
leetcode: Palindrome Partitioning (0) | 2020.12.26 |
leetcode: Contain With Most Water (0) | 2020.12.17 |