Problem


사용자 목록 user_id 와 불량 사용자 목록 banned_id 가 주어졌을 때 당첨에서 제외되어야 하는 사용자 목록의 경우의 수를 구하는 문제입니다.



Solution

최대 길이가 8 이기 때문에 완전 탐색을 해도 괜찮습니다.

사용자 목록에서 DFS 로 banned_id.length 만큼의 사용자를 뽑습니다.

뽑힌 사용자들을 banned_id 와 비교하여 일치하는 케이스를 구하면 됩니다.

중복된 값은 모두 하나의 경우가 되기 때문에 set 에 넣어서 필터링 해줍니다.

boolean isSameString(String a, String b) 함수는 두 String 의 일치 여부를 판단합니다.

boolean isBannedUsers(Set<String> set, String[] banned_id) 함수는 뽑힌 set 과 banned_id 목록을 비교하여 경우의 수에 해당되는지 판단합니다.

어차피 중복은 제거되기 때문에 banned_id 와 일대일 비교를 하기 위해 set 을 LinkedHashSet 으로 선언했습니다.



Java Code

import java.util.*;

class Solution {
    private Set<Set<String>> result;
    
    public int solution(String[] user_id, String[] banned_id) {
        result = new HashSet<>();
        dfs(user_id, banned_id, new LinkedHashSet<>());
        return result.size();
    }
    
    private void dfs(String[] user_id, String[] banned_id, Set<String> set) {
        if (set.size() == banned_id.length) {
            if (isBannedUsers(set, banned_id)) {
                result.add(new HashSet<>(set));
            }
            
            return;
        }
        
        for (String userId : user_id) {
            if (!set.contains(userId)) {
                set.add(userId);
                dfs(user_id, banned_id, set);
                set.remove(userId);
            }
        }
    }
    
    private boolean isBannedUsers(Set<String> set, String[] banned_id) {
        int i = 0;
        
        for (String user : set) {
            if (!isSameString(user, banned_id[i++])) {
                return false;
            }
        }
        
        return true;
    }
    
    private boolean isSameString(String a, String b) {
        if (a.length() != b.length()) {
            return false;
        }
        
        for (int i = 0; i < a.length(); i++) {
            if (b.charAt(i) == '*') continue;
            
            if (a.charAt(i) != b.charAt(i)) {
                return false;
            }
        }
        
        return true;
    }
}


Problem


집합이 문자열 s 로 주어졌을 때 해당 집합이 표현하는 튜플을 배열에 담아 return 하는 문제입니다.



Solution

이 문제의 핵심은 개수가 많은 값 순서대로 배열을 정렬하는 겁니다.

집합 별로 분리할 필요 없이 먼저 숫자만 구해줍니다.

s 는 '{', '}', ',' 와 숫자로만 이루어져 있다고 문제에 나와있기 때문에 중괄호를 제거하고 쉼표를 기준으로 split 해줍니다.

그리고 각 값들을 Key 로 하고 개수를 value 로 한 keyMap 을 만들어줍니다.

개수를 기준으로 정렬해야하기 때문에 keyMap 에서 key, value 를 뒤집은 valueMap 을 만들어줍니다.

그리고 answer 배열에 valueMap 에 있는 값을 순서대로 넣어주면 됩니다.

숫자의 갯수가 key 로 되어 있기 때문에 배열의 뒤에서부터 넣어주면 개수가 가장 많은 숫자가 맨 앞으로 오게 됩니다.



Java Code

import java.util.*;

class Solution {
    public int[] solution(String s) {
        Map<Integer, Integer> keyMap = new HashMap<>();
        Map<Integer, Integer> valueMap = new HashMap<>();

        String[] strs = s.replace("{", "").replace("}", "").split(",");

        for (String str : strs) {
            int key = Integer.parseInt(str);
            keyMap.compute(key, (k, v) -> v == null ? 1 : v + 1);
        }
        
        keyMap.forEach((k, v) -> valueMap.put(v, k));
        
        int n = valueMap.size();
        int[] answer = new int[n];

        for (int i = 1; i <= n; i++) {
            answer[n-i] = valueMap.get(i);
        }
        
        return answer;
    }
}


Problem


2 x 2 배열의 보드판이 주어집니다.

인형을 뽑는 위치를 정하는 moves 배열이 정해집니다.

위치는 가로만 정해지며, 해당 위치의 가장 위에 있는 인형만 뽑을 수 있습니다.

뽑은 인형은 바구니에 순서대로 쌓이며, 같은 인형이 연달아 들어온 경우 두 인형은 사라집니다.

사라진 인형의 갯수를 구해야 합니다.



Solution

단순한 구현 문제입니다.

moves 를 순회하며 인형 뽑기, 바구니에 넣기, 사라지게 하기를 순서대로 구현합니다.

보드의 높이만큼 확인해서 0 이 아닌 좌표를 찾습니다.

뽑은 인형 자리는 0 으로 바꿔줍니다.

바구니는 Stack 으로 관리하면 됩니다.

한 가지 주의할 점은 사라진 횟수가 아닌 사라진 인형의 갯수를 구하는 문제이기 때문에 answer += 2 로 해주어야 합니다.



Java Code

import java.util.*;

class Solution {
    public int solution(int[][] board, int[] moves) {
        int answer = 0;
        Stack<Integer> stack = new Stack<>();
        
        for (int move : moves)  {
            int row = move - 1;
        
            for (int col = 0; col < board.length; col++) {
                if (board[col][row] == 0) continue;
                
                int doll = board[col][row];
                board[col][row] = 0;
                
                if (!stack.isEmpty() && stack.peek() == doll) {
                    stack.pop();
                    answer += 2;
                } else {
                    stack.push(doll);
                }
                
                break;
            }
        }
        
        return answer;
    }
}


Problem


n 이 주어졌을 때 1 ~ n 까지 숫자들 중에서 첫번째로 나오는 bad version 을 찾는 문제입니다.

bool isBadVersion(version) 함수는 VersionControl 클래스에 존재하고 Solution 클래스가 상속받고 있습니다.



Solution

단순하게 O(n) 으로 제출하면 시간초과가 납니다.

이진탐색 (BinarySearch) 으로 풀어야 하는 문제입니다.

대신 중간값을 구할 때 한가지 주의해야 합니다.

단순히 mid = (lo + hi) / 2 로 짜면 lo + hi 값이 int 범위를 벗어납니다.

안전하게 mid = lo + (hi - lo) / 2 로 하면 통과하기 때문에 앞으로 이진탐색을 사용할 때는 이렇게 구하는 습관을 들이면 좋습니다.



Java Code

public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
        int lo = 1;
        int hi = n;

        while (lo <= hi) {
            int mid = lo + (hi - lo) / 2;

            if (isBadVersion(mid)) {
                hi = mid - 1;
            } else {
                lo = mid + 1;
            }
        }

        return lo;
    }
}

강화된 함수의 기능

ES6 에서는 함수의 기능을 온전하게 완성했다고 볼 수 있다.


1. 매개변수에 추가된 기능

1.1. 매개변수 기본값 (default parameter)

ES6 부터 함수 매개변수에 기본값을 줄 수 있다.

function ex(a = 1) {
    console.log({ a });
}

ex();      // { a: 1 }


기본값으로 함수 호출을 넣어줄 수도 있다.

function getDefault() {
    return 1;
}

function ex(a = getDefault()) {
    console.log({ a });
}

ex();       // { a: 1 }


1.2. 나머지 매개변수 (rest parameter)

입력된 매개변수 중에서 특정 매개변수 외의 나머지는 배열로 만들어줄 수 있다.

매개변수 개수가 가변적일 때 유용하다.

function ex(a, ...rest) { console.log({ a, rest }); } ex(1, 2, 3); // { a: 1, rest: [2, 3] }


1.3. 명명된 매개변수 (named parameter)

객체 비구조화를 이용하여 매개변수의 이름을 명시적으로 사용하며 함수를 호출할 수 있다.

매개변수의 이름과 값을 동시에 적을 수 있기 때문에 가독성이 높다.

function getValues1(numbers, greaterThan, lessThan) {
    console.log({ numbers, greaterThan, lessThan });
}

function getValues2({ numbers, greaterThan, lessThan }) {
    console.log({ numbers, greaterThan, lessThan });
}

const numbers = [10, 20, 30, 40];

getValues1(numbers, 5, 25);                             // { numbers, greaterThan: 5, lessThan: 25 }
getValues2({ numbers, greaterThan: 5, lessThan: 25 });  // { numbers, greaterThan: 5, lessThan: 25 }


1.4. 선택적 매개변수 (optional parameter)

명명된 매개변수를 응용하면 선택적 매개변수를 사용할 수도 있다.

getValues1 함수에서는 필요없는 매개변수가 있어도 undefined 로 값을 넣어주어야 한다.

매개변수의 값이 많아지면 일일히 undefined 를 넣어주어야 하고 가독성도 굉장히 떨어지게 된다.

하지만 getValues2 함수에서는 필요없는 매개변수는 코드로 적지 않고 필요한 매개변수만 넣어주면 된다.

getValues1(numbers, undefined, 25);
getValues2({ numbers, greaterThan: 5 });
getValues2({ numbers, lessThan: 25 });


2. 화살표 함수 (arrow function)

ES6 에서는 화살표를 이용하여 함수를 정의하는 방법이 새로 추가되었다.

화살표 함수를 사용하면 함수를 간결하게 작성할 수 있다.


2.1. 한줄 사용

화살표 함수는 한줄로도 간단하게 정의할 수 있다.

중괄호 블록을 사용하지 않고 바로 오른쪽에 정의하며, return 키워드를 명시적으로 정의하지 않아도 오른쪽에 있는 값이 리턴된다.

매개변수가 하나라면 소괄호도 생략 가능하다.

반환하는 값이 Object 라면 반드시 소괄호로 감싸야 한다.

const add = (a, b) => a + b;
const add5 = a => a + 5;                                        // 매개변수가 하나면 소괄호를 생략 가능하다.
const addAndReturnObject = (a, b) => ({ result: a+b });         // 반환값이 Object 라면 소괄호로 감싸준다.
const print = () => console.log("print");


2.2. 여러줄 사용

화살표 함수에 코드가 여러줄이라면 전체를 중괄호로 묶고 return 키워드를 사용한다.

const add = (a, b) => {
    if (a <= 0 || b <= 0) {
        throw new Error('must be positive number');
    }
    return a + b;
}


2.3. this 와 arguments 가 바인딩 되지 않음

화살표 함수에서는 this 와 arguments 가 바인딩 되지 않는다.

만약 arguments 가 필요하다면 나머지 매개변수 (rest parameter) 를 사용한다.

const print = (...rest) => console.log(rest);
print(1, 2);        // [1, 2]


2.4. this 바인딩 차이점

일반 함수는 호출되었을 때, 호출한 대상에 바인딩된다.

아래 코드를 통해 this 가 각각 어디를 바라보고 있는지 알 수 있다.

앞에 f. 를 붙여서 호출하면 this 가 func() 함수를 가리키고 있다.

하지만 다른 변수에 할당한 다음에 아무것도 붙이지 않고 호출하면 this 는 전역객체를 참조한다. (브라우저에서는 window)

function func() {
  this.value = 1;

  this.increase = function() {
    this.value++;
  };

  this.print = function() {
    console.log(this);
  };
}

const f = new func();
f.increase();
console.log(f.value);       // 2
f.print();                  // func { value: 2, increase: ƒ, print: ƒ }

const inc = f.increase;
const print = f.print;
inc();
console.log(f.value);       // 2
print();                    // Window { parent: Window, ... }


기존 ES5 에서는 이런 문제점을 우회하기 위해 클로저(closure) 라는 개념을 사용했다.

function func() {
  this.value = 1;
  that = this;
  
  this.increase = function() {
    that.value++;
  };

  this.print = function() {
    console.log(that);
  };
}


일반 함수는 호출할 당시의 객체에 this 바인딩 되는 대신 화살표 함수는 가장 가까운 일반 함수를 참조한다.

따라서 함수를 어디에 재할당 하던지 항상 생성되었을 당시의 일반함수를 참조하게 된다.

function func() {
  this.value = 1;

  this.increase = () => {
    this.value++;
  };

  this.print = () => {
    console.log(this);
  };
}

const f = new func();
f.increase();
console.log(f.value);       // 2
f.print();                  // func { value: 2, increase: ƒ, print: ƒ }

const inc = f.increase;
const print = f.print;
inc();
console.log(f.value);       // 3
print();                    // func { value: 2, increase: ƒ, print: ƒ }


객체와 배열의 사용성 개선

1. 간편한 생성 및 수정

1.1. 단축 속성명

단축 속성명 (shorthand property names) 로 객체 리터럴 코드를 간편하게 작성할 수 있다.

const name = 'alice';
const obj = {
    age: 21,
    name,
    getName() { return this.name; },
}
console.log(obj);   // { age: 21, name: "alice", getName: ƒ getName() }

새로 만드려는 객체의 속성명이 이미 변수로 존재하면 변수 이름만 적어주면 된다.

이때 속성명은 변수 이름과 같아진다.

속성값이 함수이면 function 키워드 없이 함수명만 적으면 된다.

마찬가지로 속성명은 함수명과 같아진다.


1.2. 계산된 속성명

계산된 속성명 (computed property names) 으로 객체의 속성명을 동적으로 결정할 수 있다.

function create1(key, value) {
  const obj = {};
  obj[key] = value;
  return obj;
}

create2 = (key, value) {}

function create2(key, value) {
  return { [key]: value };
}

console.log(create1('key1', 'value1'));       // { key1: 'value1' }
console.log(create2('key2', 'value2'));       // { key2: 'value2' }

계산된 속성명을 사용하면 create2 처럼 간결하게 코드를 짤 수 있다.

key 를 대괄호 [ ] 로 감싸는 이유는 return { key: value } 처럼 하면 속성명으로 변수값이 아닌 key 자체가 되어버리기 때문이다.


2. 속성값 간편하게 가져오기

2.1. 전개 연산자

전개 연산자(spread operator)는 배열이나 객체의 모든 속성을 풀어놓을 때 사용한다.


2.1.1. 매개변수 여러개 전달

코드의 첫 번째 줄처럼 전개 연산자를 사용하지 않는다면 매개변수의 갯수가 4 개로 고정된다.

하지만 전개 연산자를 사용한다면 numbers 배열의 갯수가 몇개든 전부 전달할 수 있다.

Math.max(1, 2, 3, 4);

const numbers = [1, 2, 3, 4];
Math.max(...numbers);


2.1.2. 배열과 객체 복사

전개 연산자를 이용하여 간단하게 배열과 객체를 복사할 수 있다.

복사된 배열이나 객체는 새로운 값이기 대문에 수정해도 기존 배열이나 객체에 영향을 주지 않는다.

배열의 경우 전개 연산자를 사용하면 그 순서가 유지된다.

const arr1 = [1, 2, 3];
const obj1 = { age: 23, name: 'alice' };

const arr2 = [...arr1];
const obj2 = { ...obj1 };
arr2.push(4);
obj2.age = 80;

console.log(arr1);      // [1, 2, 3]
console.log(arr2);      // [1, 2, 3, 4]
console.log(obj1);      // { age: 23, name: 'alice' }
console.log(obj2);      // { age: 80, name: 'alice' }


전개 연산자를 사용하면 서로 다른 두 배열이나 객체를 쉽게 합칠 수 있다.

const obj1 = { age: 21, name: 'alice' };
const obj2 = { address: 'seoul' };
const obj3 = { ...obj1, ...obj2 };      // { age: 21, name: 'alice', address: 'seoul' }

const arr1 = [1, 3, 5];
const arr2 = [2, 4, 6];
const arr3 = [...arr1, ...arr2];        // [1, 3, 5, 2, 4, 6]


ES5 까지는 중복된 속성명으로 합치면 에러가 발생했지만 ES6 부터는 허용된다.

마지막에 입력된 값이 최종값이 된다.

const obj1 = { age: 21, name: 'alice' };
const obj2 = { name: 'bob' };
const obj3 = { ...obj1, ...obj2 };      // { age: 21, name: 'bob' }
const obj4 = { ...obj2, ...obj1 };      // { name: 'alice', age: 21 }


2.2. 비구조화

2.2.1. 배열 비구조화

배열 비구조화(array destructuring)를 사용하면 배열의 여러 속성값을 변수로 쉽게 할당할 수 있다.

배열의 속성값이 왼쪽 변수에 순서대로 들어간다.

const arr = [1, 2];
const [a, b] = arr;     // a: 1, b: 2


배열 비구조화 정의 시 기본값을 설정할 수 있다.

만약 기본값도 없고 할당되는 값도 없다면 undefined 가 된다.

const arr = [1];
const [a = 10, b = 20] = arr;       // a: 1, b: 20


두 값을 교환할 수도 있다.

let a = 1;
let b = 2;
[a, b] = [b, a];        // a: 2, b: 1


일부 속성값을 무시할 수도 있다.

const arr = [1, 2, 3];
const [a, , c] = arr;       // a: 1, c: 3


나머지 값을 별도의 배열로 만들 수도 있다.

const arr = [1, 2, 3];
const [first, ...rest] = arr;       // first: 1, rest: [2, 3]
const [a, b, c, ...empty] = arr;    // a: 1, b: 2, c: 3, empty: []


2.2.2. 객체 비구조화

객체 비구조화(object destructuring)를 사용하면 여러 속성값을 변수로 쉽게 할당할 수 있다.

순서대로 들어가는 배열과 달리 Object 의 키에 맞춰서 들어간다.

그리고 Object 에 존재하는 키와 동일한 이름의 변수명을 사용해야 한다.

const obj = { age: 21, name: 'alice' };
const { age, name } = obj;      // age: 21, name: 'alice'
const { name, age } = obj;      // age: 21, name: 'alice'
const { a, b } = obj;           // a: undefined, b: undefined


임의로 다른 변수명에 할당할 수도 있다.

const obj = { age: 21, name: 'alice' };
const { age: age2, name } = obj;        // age: not defined error, age2: 21, name: 'alice'


기본값을 정의할 수 있다.

들어오는 값이 undefined 인 경우에만 기본값으로 세팅된다.

기본값 세팅과 다른 변수명에 할당을 동시에 사용할 수도 있다.

const obj = { age: undefined, name: null, grade: 'A' };
const { age: age2 = 0, name = 'noName', grade = 'F', address = 'seoul' } = obj;  
// age: not defined error, age2: 0, name: null, grade: 'A', address: 'seoul'


변수 정의: const, let

ES5 까지는 var 키워드로 변수를 정의했다.

ES6 에서는 const 와 let 을 이용하는 새로운 변수 정의 방법이 생겼다.


1. var 문제점

1.1. 함수 스코프

스코프(scope) 란 변수가 사용될 수 있는 영역을 말한다.

var 는 함수 스코프라서 함수 밖에서 사용하면 에러가 발생한다.

function error() {
    var a = 1;
}
console.log(a);     // ReferenceError: a is not defined


var 키워드 없이 변수를 선언하면 전역 변수가 된다.

function global() {
    a = 1;
}
function local() {
    console.log(a);
}
global();
local();    // 1 출력


for 문에서 선언된 변수가 사라지지 않는다.

for 문 뿐만 아니라 while, switch, if 문 등 함수 내부에서 작성되는 모든 코드가 같은 문제를 안고 있다.

for (var i = 0; i < 10; i++) {
    console.log(i);     // 0 ~ 9 출력
}
console.log(i);     // 10 출력


1.2. 호이스팅(hoisting)

var 로 정의된 변수는 해당 스코프의 최상단으로 끌어올려진다.

변수를 선언 없이 사용하면 참조 에러가 발생한다.

console.log(a);     // ReferenceError: a is not defined


하지만 다음 코드는 에러가 발생하지 않는다.

console.log(a);     // undefined
var a = 1;

호이스팅에 의해 위 코드는 아래와 같이 취급된다.

var a = undefined;
console.log(a);     // undefined
a = 1;


심지어 변수 선언 전에 값을 할당할 수도 있다.

console.log(a);     // undefined
a = 2;
console.log(a);     // 2
var a = 1;

이런 호이스팅 개념은 코드를 직관적이지 않게 만들며, 버그가 발생하여도 정확한 원인을 찾기 힘들게 만든다.


1.3. 항상 재할당 가능

var 는 재할당 가능한 변수로만 만들 수 있다.

따라서 상수처럼 고정된 값을 변수로 선언해서 이용해야 할 때에도 변경될 위험이 존재하고 있다.


2. var 문제점 해결: const, let

2.1. 블록 스코프

대부분의 언어에서 블록 스코프를 사용한다.

블록 스코프에서는 블록을 벗어나면 변수를 사용할 수 없다.

블록을 벗어나도 살아있던 var 변수와 달리 const, let 변수는 블록을 벗어나면 관리해줄 필요가 없다.

if (true) {
    const a = 0;
}
console.log(a);     // ReferenceError: a is not defined


2.2. 호이스팅(hoisting)

const, let 도 호이스팅 된다.

하지만 변수를 선언하기 전에 사용하려고 하면 에러가 발생한다.

console.log(a);      // ReferenceError: Cannot access 'a' before initialization
const a = 1;

이 때문에 const, let 은 호이스팅 되지 않는다고 생각하기 쉽다.

const, let 은 서로 다른 스코프에서 같은 이름의 변수를 사용할 때 실수를 방지해준다.


아래 코드에서 a 는 다른 블록 스코프지만 상위에 선언되었기 때문에 1 이 출력된다.

const a = 1;
{
    console.log(a);     // 1
}


하지만 아래 코드에서는 참조 에러가 발생한다.

하위 블록 스코프에서 호이스팅으로 인해 const a = 2 로 할당 되기 전까지는 사용이 불가능하다.

이처럼 같은 이름의 변수를 사용할 때의 실수를 방지해준다.

const a = 1;
{
    console.log(a);
    const a = 2;
}


2.3. 재할당 불가능 const

const 로 정의된 변수는 재할당이 불가능하다.

let 으로 선언된 변수는 재할당 가능하다.

대신 const 로 정의된 객체의 내부 속성값은 수정 가능하다.

const arr = [1, 2];
arr[0] = 3;
arr.push(4);
console.log(arr);   // [3, 2, 4]


Problem


주어진 배열의 값을 비교하여 순위를 매긴 배열을 리턴하는 문제입니다.




Solution

배열을 복사한 뒤에 정렬합니다.


배열의 값과 순위를 저장할 HashMap 을 선언합니다.


복사한 copyed 배열을 정렬한 다음에 순위를 넣습니다.


동점인 경우 값을 갱신하지 않기 위해 putIfAbsent 메소드를 사용합니다.


arr 배열을 다시 순회하면서 copyed 배열을 랭크값으로 갱신해주면 됩니다.




Java Code

class Solution {
    public int[] arrayRankTransform(int[] arr) {
        Map<Integer, Integer> map = new HashMap<>();    // value, rank
        int[] copyed = Arrays.copyOf(arr, arr.length);
        Arrays.sort(copyed);
        
        for (int value : copyed) {
            map.putIfAbsent(value, map.size());
        }
        
        for (int i = 0; i < arr.length; i++) {
            copyed[i] = map.get(arr[i]) + 1;
        }
        
        return copyed;
    }
}


+ Recent posts