이 포스팅에서는 leetcode 추천 문제 top 75를 자바스크립트로 풀어보며 자바스크립트 공부와 알고리즘 공부를 동시에 합니다. 문제와 풀이를 적을 건데, 틀린 부분이나 더욱 개선될 부분이 있다면 댓글로 달아주시면 정말 감사하겠습니다.
이 문제는 배열의 각 인덱스에서 자신을 제외한 나머지 숫자들의 곱을 넣어주는 문제입니다.
입력: [1, 2, 3, 4]
출력: [24, 12, 8, 6]
입력: [1, 0]
출력: [0, 1]
일단 처음에는 O(n^2)의 복잡도를 가진 답을 떠올렸는데, 그냥 무작정 해당 인덱스 말고 나머지를 다 곱하는 거였습니다.
대략적으로 작성한 답변은
/**
* @param {number[]} nums
* @return {number[]}
*/
var productExceptSelf = function(nums) {
return nums.map((num, index) => {
return nums.slice(0, index).concat(nums.slice(index+1, nums.length)).reduce((acc, curV) => acc * curV);
})
};
이러했습니다. 코드는 매우 짧았지만, 내부 함수들의 복잡도를 생각하면 매우 무거운 오퍼레이션들이 많습니다.
그 이후에 릿코드 자체에서 제공하는 솔루션을 보고 구현했는데
O(n)에 구현하는 방법이 있었습니다.
이 방법의 특징은 왼쪽의 첫번째 인덱스부터 시작하여 모든 숫자를 차례로 곱해나가는 왼쪽부터의 누적곱 배열과 오른쪽의 첫번째 인덱스부터 시작하여 모든 숫자를 차례로 곱해나가는 오른쪽부터의 누적곱 배열을 이용하는 방법이었습니다.
/**
* @param {number[]} nums
* @return {number[]}
*/
var productExceptSelf = function(nums) {
let leftProductArr = [];
let rightProductArr = [];
for(let i=0; i<nums.length - 1; i++){
if(leftProductArr.length === 0){
leftProductArr.push(nums[0]);
}else{
leftProductArr.push(leftProductArr[i-1] * nums[i]);
}
if(rightProductArr.length === 0){
rightProductArr.push(nums[nums.length-1]);
}else {
rightProductArr.push(rightProductArr[i-1] * nums[nums.length-1-i]);
}
}
let ls = -1;
let rs = rightProductArr.length - 1;
return nums.map((num) => {
let res = 0
if(ls >= 0 && rs >= 0){
res = leftProductArr[ls] * rightProductArr[rs];
}else if(ls >= 0 && rs < 0){
res = leftProductArr[ls];
}else if(rs >= 0 && ls < 0){
res = rightProductArr[rs];
}
ls++;
rs--;
return res;
})
};
0에 대한 처리를 제법 길게 하다보니 이렇게 소스가 길어졌습니다. 하지만 내용은 별 거 없습니다.
사실 위 방법의 문제점은 공간복잡도를 3n이나 잡아먹는다는 것입니다. 그래서 위의 방법을 개선하면
/**
* @param {number[]} nums
* @return {number[]}
*/
var productExceptSelf = function(nums) {
let answer = Array(nums.length).fill(1);
for(let i=1; i<nums.length; i++){
answer[i] = nums[i-1] * answer[i-1];
}
let right = 1;
for(let i=nums.length-1; i>=0; i--){
answer[i] = answer[i] * right;
right *= nums[i];
}
return answer;
};
이렇게 따로 Left, Right 배열을 만들지 않고 Answer 배열만으로 가능합니다.
추가적으로 숫자의 곱은 Log를 씌우면 +와 같다는 특성을 이용하여, 모든 숫자에 Log를 씌워서 더한 합을 구해놓고 해당하는 인덱스의 로그를 취한 숫자를 빼자는 의견도 있었습니다만, 0을 처리하기 까다롭습니다.
그때그때 계산하지 않고, 미리 값을 계산해놓는다(pre-caculated)는 것은 때때로 아주 큰 도움이 됩니다. 특히 숫자 계산의 경우 여러가지 법칙들이 있습니다. 이를테면 모든 숫자의 곱이나 합은 순서가 뒤죽박죽이어도 같은 결과를 내게 됩니다. 이러한 법칙을 잘 이용합시다.
이번 경우에는 위의 법칙을 이용하여 왼쪽의 누적곱, 오른쪽의 누적곱을 미리 구해서 멋진 답을 만들어낼 수 있었습니다.