[자료구조와 알고리즘] 26. Remove Duplicates from Sorted Array (Feat. JS에서의 scope)

Jane Yeonju Kim·2023년 8월 23일
post-thumbnail

문제 설명

LeetCode TopInterview150 - 26. Remove Duplicates from Sorted Array
정렬된 배열 nums가 주어지면 중복되는 값들을 제거한 다음 정렬된 상태로 nums를 변경하고,
중복되지 않은 값들의 갯수를 반환하는 문제입니다.


풀어보기

임시 배열에 중복되지 않은 값들을 순서대로 저장하고
중복되는 값은 새로운 변수 prevNum에 저장하고
임시 배열에 저장할 때 사용할 인덱스를 함수의 반환값으로 사용하자는 생각으로 접근했습니다!

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    const temp = [];
    let prevNum = 0;    // 이전 숫자
    let resIdx = 0;     // 중복 없는 숫자를 세기 위한 변수 (임시 배열의 인덱스)
    for (let i=0; i < nums.length; i++) {
        if (prevNum < nums[i] || i==0) {  // 현재 숫자 nums[i]가 이전 숫자보다 클 경우 
                                          // 또는 nums 배열의 첫번째 요소인 경우
            temp[resIdx] = nums[i];     // 임시 배열에 넣고
            prevNum = temp[resIdx++];    // 이전 숫자를 현재 숫자로 갱신
        }
        else {                           // 그 외의 경우
            prevNum = temp[resIdx-1];    // 이전 숫자를 임시 배열의 마지막 요소로 갱신
        }
    }
    for (let i=0; i < temp.length; i++) {
        nums[i] = temp[i];               // 전역 변수인 nums 배열의 요소를 하나씩 수정
    }
    return resIdx
};

temp 길이만큼 또 다시 for 반복문을 돌리느니 nums = temp로 전역 변수에 바로 변경을 하고싶지만..
당연하게도 그런 코드는 통과하지 못합니다! 😇
함수 안에서 nums = temp 코드를 실행한다면 함수 안의 로컬 변수인 nums만 수정하고
전역 변수인 nums는 전혀 수정하지 않습니다.

그런데 왜 전역 변수, 로컬 변수를 분리하는 걸까요?
순수 함수의 관점에서 생각해보면 들어온 인자값 외에 다른 외부의 상태를 변경하지 않으면서 안정성을 높이기 위해 함수 내부에서는 로컬 변수로 작업하는 것으로 이해할 수 있습니다. 한마디로 부수효과를 없애는 겁니다.

..그렇다면 다시 문제로 돌아와서 nums 배열의 요소를 하나씩 수정하는 코드는 어떻게 통과하는 걸까요? 👀
참고: 모던 자바스크립트 - 스코프
자바스크립트를 공부해신 분들이라면 스코프에 대한 공부를 이미 하신 분들이 많을 거라고 생각합니다.
예시들이 보통 전역 변수를 Number, String의 기본 데이터 타입을 사용하고 있습니다.
전역 변수가 참조 타입이라면 어떻게 되는 걸까요?

전역변수가 참조타입인데 변경하는 경우

var a = {"key": "value"};
let b = {"key": "valu"};
const c = {"key":"val"};

function changeObject(b) {
  b = {"newKey": "newValue"};
}
function addKey(b) {
  b["newKey"] = "newVal";
}
function delKey(b) {
  delete b["key"];
}
changeObject(a);
changeObject(b);
changeObject(c);
console.log(a,b,c);
/* 
[object Object] {
  key: "value"
}
[object Object] {
  key: "valu"
}
[object Object] {
  key: "val"
}
*/
delKey(a);
delKey(b);
delKey(c);
console.log(a,b,c);
/*
[object Object] { ... }
[object Object] { ... }
[object Object] { ... }
*/
addKey(a);
addKey(b);
addKey(c);
console.log(a,b,c);
/*
[object Object] {
  newKey: "newVal"
}
[object Object] {
  newKey: "newVal"
}
[object Object] {
  newKey: "newVal"
}
*/

스코프와 상관없이 Object 변수(참조 타입)의 메모리 주소는 변경되지 않고(위의 예시에서 changeObject함수를 실행해도 전역 변수는 전혀 변경되지 않습니다.) 전역 변수에 새로운 요소를 추가하고 삭제하는 것은 가능합니다.
여기서 var, let, const로 선언하는 것이 아무 차이가 없다는 점도 중요합니다!
보안 이슈로 const로 선언하지만 함수 단위에서 접근해서 요소를 수정할 수 있다는 점을 기억하고 있어야겠습니다. 😇
그리고 object의 경우처럼 array에도 똑같이 적용됩니다.


더 좋은 코드

var removeDuplicates = function(nums) {
    let i = 0;
    for (let j = 1; j < nums.length; j++) {
        if (nums[j] != nums[i]) 
            nums[++i] = nums[j];
    }
    return ++i;
}

저는 임시 배열을 선언했지만, 임시 배열 없이 i와 j의 투 포인터로 한 배열에 접근해서 풀이하는 좋은 코드를 발견했습니다!
전역 변수 i로 첫번째 인덱스, 지역 변수 j로 두번째 인덱스부터 시작하여 j만 증가시키면서 반복문을 돌리고,
값이 같지 않을때(중복되지 않은 숫자가 나올때) i를 증가시키면서 nums배열에 그 값을 저장하는 방법입니다.
마지막에 반환할때는 중복되지 않은 숫자의 갯수를 반환해야 하기때문에 인덱스값에 1을 더해줍니다.


결과적으로 유의미한 차이를 보이지는 않지만 가독성이 훨씬 좋아졌습니다.. 😇

마무리

제가 한참동안 헷갈렸던 개념인 스코프와 참조 타입을 설명하고 싶은 욕심에 글이 많이 길어졌습니다.. 😇만
투 포인터 알고리즘을 하나의 배열에 적용하는 좋은 코드를 발견해서 복습할 수 있는 좋은 기회였습니다.

profile
안녕하세요! 김개발자입니다 👩‍💻 🐈

0개의 댓글