Data Stream as Disjoint Intervals
문제 내용
Class 를 만드는 문제이다.
Class의 목표는 다음과 같다.
- addNum(int) : integer를 입력 받는다
- getIntervals() : 입력받은 integer의 배열이 1의 차이로 연결되어 있다면 [start,end]로 연결되어 있는 값의 시작값과 연결이 끝나는 끊값으로 구성된 2차원 배열을 return 한다.
접근 방법
해당 문제는 다음과 같은 constraints를 갖는다.
- $ 0 <= val <= 10^4 $
즉, val은 최대 10000개까지 밖에 운영 되지 않는다.
이정도면 큰 값이 되지 않기 때문에 array로 cover 가능하다.
우선 Brute force로 해당 문제를 해결해 보자.
var SummaryRanges = function() {
this.arr = new Array(10001).fill(false);
};
/**
* @param {number} val
* @return {void}
*/
SummaryRanges.prototype.addNum = function(val) {
this.arr[val] = true;
};
/**
* @return {number[][]}
*/
SummaryRanges.prototype.getIntervals = function() {
const result = new Array();
let flag = false;
let startIdx = -1;
for(let i = 0; i < 10001; i++){
if(this.arr[i]){
if(!flag){
startIdx = i;
flag = true;
}
} else {
if(flag){
result.push([startIdx,i-1]);
flag = false;
}
}
}
return result;
};
위의 코드를 보면 array를 10001로 설정하고 있는데 그 이유는 입력 값에 10001이 절대 들어오지 않기 때문에 loop를 종료 시키는 처리를 쉽게 할 수 있기 때문이다.
10001개의 array를 false로 만들어 두고, 가장 왼쪽 0부터 100001번까지 순차적으로 start인지 end인지를 flag로 check하면서 결과 값에 array를 push해주면 된다.
Time complexity로 보자면, getIntevals는 $ O(1) $ 이된다. Constant Time인데 그 이유는 loop가 10001개로 고정되었기 때문이다. 하지만 실제 속도를 보자면 $ O(N) $ 이라고 보는 것이 타당하다.
Range Memorization
이것을 조금더 개선해 볼수 없을까?
각 index의 value값을 true, false말고 마지막 값으로 연결해 보자.
예를 들자면 아래와 그림과 같이 작동하게 해보자.
10000개의 index와 각 index 값을 -1로 지정했다.
2를 넣어 주고 해당 위치에 2의 값을 표시해 준다.
3을 넣어주고 만약 뒷쪽에 연결되는 값이 있다면 현재 자신의 값으로 업데이트 시켜준다.
1을 넣었을 때 뒤에 인접한 값이 있다면 자신의 값을 인접한 뒤 cell의 값으로 업데이트 시켜준다.
이런 방법으로 12의 값도 업데이트 가능하다.
정리해보자면 다음과 같다.
- 0 < arr[idx + 1] then arr[idx] = arr[idx -1]
- while 0 < arr[idx - 1] then arr[idx-1] = arr[idx]
코드로는 다음과 같다.
var SummaryRanges = function() {
this.arr = new Array(10001).fill(-1);
};
/**
* @param {number} val
* @return {void}
*/
SummaryRanges.prototype.addNum = function(val) {
if(0 <= this.arr[val]) return;
this.arr[val] = val;
if(0 < this.arr[val + 1]){
this.arr[val] = this.arr[val+1];
}
while(0 < val && 0 <= this.arr[val-1]) this.arr[val-1] = this.arr[val--];
};
/**
* @return {number[][]}
*/
SummaryRanges.prototype.getIntervals = function() {
const result = new Array();
for(let i = 0; i < 10001; i++){
if(0 <= this.arr[i]){
result.push([i, this.arr[i]]);
i = this.arr[i] + 1;
}
}
return result;
};
time complexity는 여전히 $ O(N) $ 이지만 속도에 증가를 보인다.
여기에 반대쪽 Memorization을 추가하면 하기에 보여줄 Binary search를 사용하는 방법과 준하는 속도를 보여준다.
역방향 Memory를 추가한 모습이다.
이것을 코드로 추가하면 아래와 같다.
var SummaryRanges = function() {
this.arr = new Array(10001).fill(-1);
this.re = new Array(10001).fill(10000);
};
/**
* @param {number} val
* @return {void}
*/
SummaryRanges.prototype.addNum = function(val) {
if(0 <= this.arr[val]) return;
this.arr[val] = val;
if(0 < this.arr[val + 1]){
this.arr[val] = this.arr[val+1];
}
while(0 < val && 0 <= this.arr[val-1]) this.arr[val-1] = this.arr[val--];
const lastIdx = --val;
while(0 <= val && this.arr[val] < 0) this.re[val--] = lastIdx;
};
/**
* @return {number[][]}
*/
SummaryRanges.prototype.getIntervals = function() {
const result = new Array();
for(let i = 0; i < 10001; i++){
if(0 <= this.arr[i]){
result.push([i, this.arr[i]]);
i = this.arr[i] + 1;
} else {
i = this.re[i];
}
}
return result;
};
여전히 $ O(N) $ 이지만 getIntervals의 속도가 $ O(grouping 갯수) $ 의 속도로 빨라진다.
Binary Search (Heap)
위와 속도는 거의 비슷하지만 Memory를 줄이는 방법이 있다.
결국 위의 getIntervals에서 필요한건 sorting이 된 value의 array라는 점에서 착안한 것이다.
즉, [2,5,7] 이 있으면 3번 loop를 돌아서 결과 값을 얻을 수 있다. 여기에 3이 들어가면 [2,3,5,7] 이런 순서에 맞는 value array를 구하는게 핵심이다.
가장 쉽게 이러한 insertion 행위가 가능한 것은 Binary Search이다.
예를 들어서 [2,5,7]에서 3이 들어갈 자리는
2와 5사이 이다 이 Position을 가장 빠르게 찾는 방법은 Lower Bound를 찾는 것이다.
2021.06.16 - [Problem Solving] - Binary Search Lower Bound와 Upper Bound
상기 내용에서 Lower bound를 꼭 읽어보자.
해당 코드는 아래와 같다.
var SummaryRanges = function() {
this.arr = new Array();
};
/**
* @param {number} val
* @return {void}
*/
SummaryRanges.prototype.addNum = function(val) {
if(this.arr.length === 0) {
this.arr.push(val);
return;
}
let left = 0;
let right = this.arr.length;
while(left < right){//1
const mid = left + Math.floor((right - left)/2);
if(this.arr[mid] === val){
return;
}
if(val < this.arr[mid]){
right = mid;
} else {
left = mid + 1;
}
}
this.arr.splice(left, 0, val);
};
/**
* @return {number[][]}
*/
SummaryRanges.prototype.getIntervals = function() {
const result = new Array();
let start = this.arr[0];
let end = this.arr[0];
for(let i = 1; i < this.arr.length; i++){
if(1 < this.arr[i] - end) {
result.push([start,end]);
start = this.arr[i];
end = this.arr[i];
} else {
end = this.arr[i];
}
}
result.push([start,end]);
return result;
};
add Num의 $ O(logN) $ 으로 빨라지게 된다.
이와 같이 순서를 유지하면서 Next 값을 얻을 수 있는 자료구조가 하나더 있다 바로 Heap이다.
Min Heap을 사용하면 바로 다음에 오는 값을 찾을 수 있다.
2020.09.14 - [Problem Solving] - Javascript Priority Queue 오름 차순
위 링크로 설명을 대체하자
이것도 결론은 $ O(logN) $으로 동일함으로 해당 부분을 추가 개발하진 않았다.
'Problem Solving' 카테고리의 다른 글
Longest Repeating Character Replacement (0) | 2021.08.13 |
---|---|
Power of Four (0) | 2021.08.13 |
Hamming Distance (0) | 2021.08.09 |
Different Ways to Add Parentheses (0) | 2021.08.03 |
Confirmation Rate (0) | 2021.08.02 |