Jump Game

문제

Given an array of non-negative integers, you are initially positioned at the first index of the array.

Each element in the array represents your maximum jump length at that position.

Determine if you are able to reach the last index.

For example:
A = [2,3,1,1,4], return true.

A = [3,2,1,0,4], return false.

0번 인덱스를 시작점으로, 각 인덱스의 값만큼 점프해서 마지막 인덱스까지 갈 수 있는지 판별하는 문제

풀이

먼저 접근한 방법은 길이가 nums와 같은 새로운 배열에 0을 할당한 뒤에,
nums와 같은 인덱스에서부터 인덱스의 값만큼 다음 인덱스에 1씩 더해주는 방법으로 생각했음.( 이해안돼도 상관X, 쓰레기같은 방법이였음.. )

const canJump = nums => {
    let arr = [];
    for ( let i = 0; i < nums.length; i++ ) {
        arr.push(0);
    }
    for ( let i = 0; i < nums.length-1; i++ ) {
        for ( let j = i+1; j <= i+nums[i]; j++ ) {
            arr[j]++;
        }
    }
    return arr.lastIndexOf(0) === 0 ? true :false;
};

하다가 말아서 정확히는 아니고 대충 위와같이 코드를 작성했고, 시간복잡도는 n^2이 나오는데 테스트케이스에 n^2으로는 통과할 수 없을 정도로 긴 케이스가 있어서, 고민하다가 검색해보니 Greedy Algorithm이라는 방법이 있어 코드를 보고 아래와 같이 작성하니 시간복잡도 n으로 통과.

const canJump = nums => {
    let lastPos = nums.length-1;
    for ( let i = nums.length-1; i >= 0; i-- ) {
        if ( i + nums[i] >= lastPos ) {
            lastPos = i;
        }
    }
    return lastPos === 0;
};

Greedy Algorithm이란 최적해를 구하는 데에 사용되는 근사적인 방법으로, 여러 경우 중 하나를 결정해야 할 때마다 그 순간에 최적이라고 생각되는 것을 선택해 나가는 방식으로 진행하여 최종적인 해답에 도달한다. 순간마다 하는 선택은 그 순간에 대해 지역적으로는 최적이지만, 그 선택들을 계속 수집하여 최종적(전역적)인 해답을 만들었다고 해서, 그것이 최적이라는 보장은 없다.

매트로이드라는 구조를 가진 문제에서는 그리디 알고리즘으로 항상 최적해를 찾을 수 있다.

위의 코드를 설명해보면

nums의 마지막 인덱스 값을 lastPos에 할당해놓고, 그 값부터 시작해서 lastPos를 0까지 도달하게 하는 것. 반복문 전체를 도는것이 아닌, 최적의 판단으로 횟수를 최대한 줄여준다.


Valid Parentheses

문제

Given a string containing just the characters ‘(‘, ‘)’, ‘{‘, ‘}’, ‘[‘ and ‘]’, determine if the input string is valid.

The brackets must close in the correct order, “()” and “()[]{}” are all valid but “(]” and “([)]” are not.

괄호들이 개수와 순서가 맞게 적혀 있는지 판별

풀이

const isValid = s => {
    while ( s.indexOf("()") !== -1 || s.indexOf("{}") !== -1 || s.indexOf("[]") !== -1 ) {
        if ( s.indexOf("()") !== -1 ) {
            s = s.replace(s[s.indexOf("()")]+s[s.indexOf("()")+1], '')
        }
        if ( s.indexOf("{}") !== -1 ) {
            s = s.replace(s[s.indexOf("{}")]+s[s.indexOf("{}")+1], '')
        }
        if ( s.indexOf("[]") !== -1 ) {
            s = s.replace(s[s.indexOf("[]")]+s[s.indexOf("[]")+1], '')
        }
    }
    return s === '' ? true: false
};

반복문으로 쌍이 없을때까지 s의 길이를 줄여나가고, 빈문자열이 된다면 true, 아니라면 false 리턴

너무 쉬운문제만 골라서 풀고 있는 것 같다.

쉬운거 아무리 풀어봐야 소용 없으니, 내일부터 medium단계에 도전해야겠다.

선택 정렬 ( selection sort )

선택 정렬이란?

선택 정렬(選擇整列, selection sort)은 제자리 정렬 알고리즘의 하나로, 일반적으로 사람이 어떤 것을 크기 순서대로 정렬할 때 사용하는 방법과 유사한 방법이다. 나열된 것 중에 가장 작은 또는 큰 것을 선택해 앞 또는 끝으로 보내는 작업을 반복하면 최종적으로 크기 순서대로 정렬이 되는 방식이다.

비교하는 것이 상수 시간에 이루어진다는 가정 아래, n개의 주어진 리스트를 이와 같은 방법으로 정렬하는 데에는 Θ(n^2) 만큼의 시간이 걸린다.

선택 정렬은 다음과 같이 작동한다.

  1. 주어진 리스트 중에 최솟값을 찾는다.
  2. 그 값을 맨 앞에 위치한 값과 교체한다.
  3. 맨 처음 위치를 뺀 나머지 리스트를 같은 방법으로 교체한다.

코드와 설명

const selectionSort = arr => {
    let minIndex, temp;
    for ( let i = 0; i < arr.length-1; i++ ) { // 처음부터 마지막 하나전까지를 도는 반복문
        minIndex = i // 정렬되지 않은 배열 중 최솟값의 인덱스를 minIndex에 담음
        for ( let j = i+1; j < arr.length; j++ ) { 
            if ( arr[j] < arr[minIndex] ) { // 현재의 최솟값보다 작은 값이 있다면 minIndex를 해당 인덱스로 변경
                minIndex = j
            }
        }
        temp = arr[i]; // 정렬되지 않은 배열중 최솟값과 가장 앞의 값의 위치를 바꿔줌.
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;
}

console.log(selectionSort([3,4,1,2]));

선택 정렬과 비슷하게 코드도 짧고 엄청 쉽지만, 그만큼 성능이 안좋아 실제로는 거의 안쓴다고 보면 된다고 함.

직관적이지만 인풋이 크다면 정렬할 때 좋지 않은 방법이다.


거품 정렬 ( bubble sort )

거품 정렬이란?

거품 정렬(Bubble sort)은 두 인접한 원소를 검사하여 정렬하는 방법이다. 시간 복잡도가 O(n^2)로 상당히 느리지만, 코드가 단순하기 때문에 자주 사용된다. 원소의 이동이 거품이 수면으로 올라오는 듯한 모습을 보이기 때문에 지어진 이름이다.

이전 게시물까지는 한글로 정렬 이름을 불렀는데, 거품 정렬은 어감이 뭔가 어색해서 다음부터는 버블 소트라고 해야겠다ㅋㅋ

버블 소트는 다음과 같이 작동한다.

  1. 배열의 원소를 두개씩 순서대로 비교하며 앞의 원소가 더 크다면 뒤의 원소와 자리를 바꾼다.
  2. 한번 배열을 돌고나면 최소 마지막 한개는 정렬이 완료된다.
  3. 배열 전체가 정렬이 될 때까지 반복문을 돌려 리턴.

코드와 설명

const bubbleSort = arr => {
    let temp;
    for ( let i = 0; i < arr.length-1; i++ ) { // 배열의 첫번째 부터 마지막에서 하나 전까지 돌게하는 반복문
        for ( let j = 0; j < arr.length-1-i; j++ ) { // 원소를 두개씩 비교해주는 반복문
            if ( arr[j] > arr[j+1] ) {
                temp = arr[j];  // 세줄로 i번째와 i+1번째 위치를 바꿔줌
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
    return arr;
}

console.log(bubbleSort([3,4,1,2]))

보다시피 코드도 짧고 엄청 쉽지만, 그만큼 성능이 안좋아 실제로는 거의 안쓴다고 보면 된다고 함.

여태 했던 것중에 가장 쉽지만 쓸모없는 정렬 방법


Palindrome Number

문제

Determine whether an integer is a palindrome. Do this without extra space.

추가로 공간을 사용하지 않고서 팰린드롬 숫자를 판별 ( 앞뒤로 대치인 숫자 )

풀이

const isPalindrome = x => {
    if ( x < 0 ) return false; // 음수인 경우일 때 false 를 리턴
    const str = x.toString(); // 숫자를 문자로 변경
    const middle = parseInt(str.length/2); // 중간값 잡음
    for ( let i = 0; i < middle; i++ ) { // 포문으로 처음과 끝값을 비교하고 한칸씩 비교해서 모두 같으면 true 리턴
        if ( str[i] !== str[str.length-i-1] ) {
            return false;
        }
    }
    return true;
};

주석의 내용이 전부임.


Roman to Integer

문제

Given a roman numeral, convert it to an integer.

Input is guaranteed to be within the range from 1 to 3999.

로마숫자를 정수로 바꾸는 문제

풀이

const romanToInt = s => {
    const roman = {'I':1, 'V':5, 'X':10, 'L':50, 'C':100, 'D':500, 'M':1000};
    if ( s.length === 1 ) {
        return roman[s]
    }
    let result = 0;
    const special = { 'IV': 4, 'IX': 9, 'XL': 40, 'XC': 90, 'CD': 400, 'CM': 900 }
    for ( let i = 0; i < s.length-1; i++ ) {
        if ( special[s[i]+s[i+1]] ) {
            result += special[s[i]+s[i+1]];
            s = s.replace(s[i]+s[i+1], '');
            i = -1
        }
    }
    for ( let j = 0; j < s.length; j++ ) {
        result += roman[s[j]]
    }
    return result
};

문제는 어렵지 않은데 예외처리를 일일이 해줘야하는게 귀찮았음.

먼저 각각 문자가 뜻하는 숫자를 roman에 담고, 특별한 경우를 따로 special에 담는다.

우선 길이가 1개일 때 roman에서 매칭되는 값을 리턴해주고

길이가 2개 이상인 경우에는 먼저 special에 해당하는 문자가 있는지 찾아서 result에 더해준 뒤에 문자열에서 삭제해주는 포문을 하나 돌리고

남은 값들을 roman에서 매칭시켜 result에 더해주고 리턴하면 끝

객체로 묶는 생각을 바로 못해서 일일이 if문으로 했다가, 나중에 수정했음


합병 정렬 ( merge sort )

합병 정렬이란?

합병 정렬 또는 병합 정렬(merge sort)은 O(n log n) 비교 기반 정렬 알고리즘이다. 일반적인 방법으로 구현했을 때 이 정렬은 안정 정렬에 속하며, 분할 정복 알고리즘의 하나이다. 존 폰 노이만이 1945년에 개발했다.

합병 정렬은 다음과 같이 작동한다.

  1. 리스트의 길이가 0 또는 1이면 이미 정렬된 것으로 본다. 그렇지 않은 경우에는
  2. 정렬되지 않은 리스트를 절반으로 잘라 비슷한 크기의 두 부분 리스트로 나눈다.
  3. 각 부분 리스트를 재귀적으로 합병 정렬을 이용해 정렬한다.
  4. 두 부분 리스트를 다시 하나의 정렬된 리스트로 합병한다.

코드와 설명

const mergeSort = arr => {
    if ( arr.length < 2 ) return arr; // 배열의 길이가 1개일 때 배열을 리턴
    const middle = parseInt(arr.length/2); // 배열의 중간지점을 잡음
    const left = arr.slice(0, middle); // 0부터 중간까지
    const right = arr.slice(middle, arr.length); // 중간부터 끝까지
    return merge(mergeSort(left), mergeSort(right)); // merge라는 정렬해주는 함수에 인자로 보내줌
}

const merge = (left, right) => {
    let result = []; // 합병 결과 리턴값
    while ( left.length && right.length ) {
        left[0] > right[0] ? result.push(right.shift()) : result.push(left.shift()); // left, right 둘중 하나의 원소가 없을 때 까지 대소 비교 후 result 에 삽입
    }
    while ( left.length ) {
        result.push(left.shift()); // left의 남은 원소 삽입
    }
    while ( right.length ) {
        result.push(right.shift()); // right의 남은 원소 삽입
    }
    return result; // 리턴
}

console.log(mergeSort([3, 2, 1]))

우선 mergeSort라는 함수로 배열을 받으면, 배열의 길이가 1이될 때까지 반씩 쪼갬과 동시에 쪼갠 배열을 merge라는 함수에 넣어 순서대로 정렬한 뒤에 다시 합쳐준다.

사실 짜고도 완벽하게 정렬 과정이 이해가 안된다. 조금더 봐야할 것 같음ㅜ

// 추가 내용 : 직접 코드 진행 과정을 써보니까 이해가 되었음

mergeSort([3,2,1]) 을 기준으로 과정을 나열해 보면

mergeSort([3,2,1])

merge(mergeSort([3]), mergeSort([2,1])) // left, right 로 나뉘어져 리턴

merge([3], mergeSort([2,1])) // left 값, 즉 [3]은 배열의 길이가 1이므로 배열 자체를 리턴

merge([3], merge([2],[1])) // right값은 다시한번 left값 [2], right값 [1] 로 나뉘어짐

merge([3], [1,2]) // merge 함수에 의해 merge([2],[1]) 이 [1,2]로 리턴

[1,2,3] // merge 함수에 의해 merge([3], [1,2]) 가 [1,2,3]으로 리턴

위와 같이 정렬이 된다.

쉽다 !


'자료구조, 알고리즘 > JS 자료구조' 카테고리의 다른 글

3. 큐 ( Queue )  (0) 2017.11.17
2. 스택 ( Stack )  (0) 2017.11.15
1. 연결 리스트 ( Linked List )  (0) 2017.11.10

+ Recent posts