본문 바로가기

코딩테스트/알고리즘

예제로 배우는 알고리즘 - 배열

배열 병합

 

예제 - LeetCode 80. Merge Sorted Array

https://leetcode.com/problems/merge-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  •  정렬된 두 배열을 병합하기

 

2. 요구사항 분석

  • 입력
    • nums1.length = m + n
    • nums2.length = n
    • 0 <= m, n <= 200
    • 1 <= m + n <= 200
    • 각 문자열의 요소는 0 또는 1
    • -1,000,000,000 <= nums1[i], nums2[i] <= 1,000,000,000 -> int 범위
  • 출력
    • 없음, (nums1 배열에 채우기)

 

3. 해결 아이디어

  • nums1, nums2에서 큰 값 순서대로 비교하면서 nums1 뒤에서 부터 채우기

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도
    • nums1, nums2를 한번씩 반복하므로 O(n + m)
  • 공간 복잡도
    • 고정된 개수의 변수만 사용하므로 O(1)

 

5. 구현

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        // 1. 인덱스를 표시할 변수 할당
        int i1 = m - 1;  // nums1의 마지막 유효 원소 인덱스
        int i2 = n - 1;  // nums2의 마지막 인덱스
        int i = m + n - 1;  // 병합 결과를 저장할 nums1의 끝 인덱스

        // 2. 뒤에서부터 비교하여 큰 값을 nums1의 뒤쪽에 삽입
        while (i1 >= 0 && i2 >= 0) {
            if (nums1[i1] > nums2[i2]) {
                nums1[i--] = nums1[i1--];
            } else {
                nums1[i--] = nums2[i2--];
            }
        }

        // 3. nums2의 남은 요소가 있다면 nums1에 복사
        while (i2 >= 0) {
            nums1[i--] = nums2[i2--];
        }
    }
}

 

특정 요소 제거

 

예제 - LeetCode 27. Remove Element

https://leetcode.com/problems/remove-element/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  •  배열에서 특정 값을 제거하고, 나머지 값을 앞으로 당기기(뒤에는 어떤 값이 있어도 됨)

 

2. 요구사항 분석

  • 입력
    • 0 <= nums.length, val <= 100
    • 0 <= nums[i] <= 50
  • 출력
    • 제거 후 남은 요소의 갯수

 

3. 해결 아이디어

  • 투 포인터
    1. 요소의 값을 확인할 인덱스, 요소를 저장할 인덱스 두 개의 포인터를 이용
    2. 특정 요소가 아닌 경우 배열에 채우고 특정 요소인 경우 요소의 값을 확인하는 인덱스만 이동

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도
    • nums 배열을 한번 순회하므로 O(n)
  • 공간 복잡도
    • 고정된 개수의 변수만 사용하므로 O(1)

 

5. 구현

class Solution {
    public int removeElement(int[] nums, int val) {
        // 1. 배열을 채우는 인덱스 변수 할당
        int i1 = 0;

        // 2. 배열을 전체 순회하며 주어진 값이 아닌 경우 배열에 채우기
        for (int i2 = 0; i2 < nums.length; i2++) {
            if (nums[i2] != val) {
                nums[i1++] = nums[i2];
            }
        }
        return i1;
    }
}

 

정렬된 배열에서 중복된 값 제거

 

예제 - LeetCode 26. Remove Duplicates from Sorted Array

https://leetcode.com/problems/remove-duplicates-from-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150

 

 

1. 문제 이해

  •  오름차순으로 정렬된 배열에서 중복된 값을 제거한 후 각 원소가 하나씩 존재하도록 수정(정렬 유지)

 

2. 요구사항 분석

  • 입력
    • 1 <= nums.length <= 30,000
    • -100 <= nums[i] <= 100
  • 출력
    • 제거 후 남은 요소의 갯수

 

3. 해결 아이디어

  • 투 포인터
    1. 요소의 값을 확인할 인덱스, 요소를 저장할 인덱스 두 개의 포인터를 이용
    2. 배열의 이전 값과 비교하며 다를 경우(중복이 아님) 값 갱신, 같을 경우 요소의 값을 확인할 인덱스만 한칸 이동 

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도
    • nums 배열을 한번 순회하므로 O(n)
  • 공간 복잡도
    • 고정된 개수의 변수만 사용하므로 O(1)

 

5. 구현

class Solution {
    public int removeDuplicates(int[] nums) {
        // 1. 요소를 저장할 인덱스 할당
        int i = 0;
        
        // 2. 배열을 순회하면서 중복된 요소 제거, j는 배열의 값을 확인할 인덱스
        for (int j = 1; j < nums.length; j++) {
            // 2-1) 이전 값과 동일하지 않다면 중복이 아니므로, 요소값 갱신
            if (nums[j] != nums[i]) {
                nums[++i] = nums[j]; 
            }
            // 2-2) 이전 값과 동일할 경우 i는 유지하고 j만 이동
        }

        return i + 1;
    }
}

 

추가 예제 - LeetCode 26. Remove Duplicates from Sorted Array 2

https://leetcode.com/problems/remove-duplicates-from-sorted-array-ii/description/?envType=study-plan-v2&envId=top-interview-150

 

이전의 예제에서 이전값 대신에 전전값을 확인하도록 수정하면 풀이 가능

 

구현

class Solution {
    public int removeDuplicates(int[] nums) {
        // nums.length가 2인 경우는 항상 성립
        if (nums.length <= 2) return nums.length;

        // 1. 요소를 저장할 인덱스 할당, 2개까지 가능하므로 2번 인덱스부터 확인
        int i = 2;  
        // // 2. 배열을 순회하면서 중복된 요소 제거(2개까지 가능), j는 배열의 값을 확인할 인덱스
        for (int j = 2; j < nums.length; j++) {
            // 2-1) 전전 값과 동일하지 않다면 중복이 아니므로, 요소값 갱신
            if (nums[j] != nums[i - 2]) {
                nums[i++] = nums[j];
            }
            // 2-2) 전전 값과 동일할 경우 i는 유지하고 j만 이동
        }

        return i;
    }
}

 

과반수 요소 찾기

 

예제 - LeetCode 169. Majority Element

https://leetcode.com/problems/majority-element/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  •  배열에서 과반수 이상 등장하는 요소 찾기

 

2. 요구사항 분석

  • 입력
    • 1 <= nums.length <= 50,000
    • - 1,000,000,000 <= nums[i] <= 1,000,000,000
  • 출력
    • 과반수 이상 등장하는 요소(반드시 존재)

 

3. 해결 아이디어

  • 보이어-무어 투표 알고리즘
    1. 후보를 하나 정하고 count 계산
    2. 후보와 같으면 count++, 다르면 count--
    3. count = 0일때는 새로운 후보로 갱신
    4. 마지막에 남은 후보가 과반수 이상 등장하는 요소
    5. 배열을 한번 더 순회하며 과반수인지 확인(현재 문제에서는 반드시 존재하므로 생략 가능)

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도
    • nums 배열을 한번 순회하므로 O(n)
  • 공간 복잡도
    • 고정된 개수의 변수만 사용하므로 O(1)

 

5. 구현

class Solution {
    public int majorityElement(int[] nums) {
    	// 1. 후보와 후보의 갯수를 저장할 변수 할당
        int candidate = 0;
        int count = 0;
		
        // 2. 배열을 순회하면 count 계산 및 후보 갱신
        for (int num : nums) {
        	// 2-1) count가 0일 때는 후보 갱신
            if (count == 0) {
                candidate = num;
            }
            // 2-2) 현재 값과 후보가 같으면 +1, 다르면 -1
            count += (num == candidate) ? 1 : -1;
        }

        return candidate;
    }
}

 

배열 회전

 

예제 - LeetCode 189. Rotate Array

https://leetcode.com/problems/rotate-array/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  •  배열을 오른쪽으로 k번 회전하기

 

2. 요구사항 분석

  • 입력
    • 1 <= nums.length <= 100,000
    • nums[i]는 int 범위
    • 0 <= k <= 100,000
  • 출력
    • 오른쪽으로 k번 회전한 배열

 

3. 해결 아이디어

  • 배열 복사 후 덮어 쓰기
    1. 같은 크기의 배열 만들기
    2. k번 이동한 인덱스에 따라 배열에 값 채우기
  • 배열 뒤집기
    1. 배열 전체 뒤집기
    2. 앞에서 부터 k개 뒤집기
    3. 나머지(n-k)개 뒤집기
  • 순환 교체
    1. 현재 인덱스부터 (i + k) % n만큼 이동해가면서 값 변경
    2. 처음으로 돌아오면 다음 인덱스 부터 순환
    3. 모든 값이 순환되었다면 종료

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도 
    • 배열 복사 후 뒤집기 : nums 배열을 한번 순회하므로 O(n)
    • 배열 뒤집기 : nums 배열을 세번 순회하므로 O(n)
    • 순환 교체 : nums 배열을 사이클(n, k의 최대공약수)만큼 순회하므로 O(n)
  • 공간 복잡도
    • 배열 복사 후 뒤집기 : 복사할 배열이 필요하므로 O(n)
    • 배열 뒤집기 : 고정된 개수의 변수만 사용하므로 O(1)
    • 순환 교체 : 고정된 개수의 변수만 사용하므로 O(1)

공간 복잡도에 이점이 있고, 직관적인 배열 뒤집기를 선택

 

5. 구현

class Solution {
    public void rotate(int[] nums, int k) {
        // 1. 배열의 크기와 순환할 값 할당
        int n = nums.length;
        k = k % n;  // 1-1) k가 n보다 클 경우를 고려하여 나머지 계산

        // 2. 전체를 뒤집고, 두 구간으로 나눠 다시 뒤집기
        reverse(nums, 0, n - 1);       // 전체 뒤집기
        reverse(nums, 0, k - 1);       // 앞 k개 뒤집기
        reverse(nums, k, n - 1);       // 나머지 뒤집기
    }

    // 배열 뒤집기 함수
    private void reverse(int[] nums, int start, int end) {
        // 앞, 뒤 인덱스를 각각 더하고 빼가며 값 변경하기
        while (start < end) {
            int temp = nums[start];
            nums[start++] = nums[end];
            nums[end--] = temp;
        }
    }
}

 

H-Index

 

예제 - LeetCode 274. Rotate Array

https://leetcode.com/problems/h-index/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  • H-Index(인용 수가 h 이상인 논문이 h편 이상)을 만족하는 가장 큰 h 구하기

 

2. 요구사항 분석

  • 입력
    • 1 <= citations.length <= 5,000
    • 0 <= citations[i] <= 1,000
  • 출력
    • h-index(정수)

 

3. 해결 아이디어

  1. 인용 수 기준으로 오름차순 정렬
  2. 현재 논문보다 인용 횟수가 많은 논문의 갯수(h) 구하기
  3. 현재 논문의 인용 횟수가 h이상이면 최대 h-index

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도 
    • 배열의 정렬이 필요하므로 O(nlogn)
  • 공간 복잡도
    • 고정된 개수의 변수만 사용하므로 O(1)

5. 구현

class Solution {
    public int hIndex(int[] citations) {
        int n = citations.length;

        // 1. 배열 오름차순 정렬
        Arrays.sort(citations);

        // 2. 배열을 순회하며 h-index 찾기
        for (int i = 0; i < n; i++) {
            // 2-1) 현재 논문보다 인용 횟수가 많은 논문의 갯수는 n - i
            int h = n - i;
            // 2-2) 현재 논문의 인용 횟수가 h를 넘었으면 최대 h-index(i가 커질수록 h가 작아지므로)
            if (citations[i] >= h) {
                return h;
            }
        }

        return 0;
    }
}

 

접두사-접미사

 

예제 - LeetCode 238. Product of Array Except Self

https://leetcode.com/problems/product-of-array-except-self/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  • answer[i]는 nums[i]를 제외한 나머지 모든 요소의 곱이 되는 배열만들기
  • 전체 곱 계산 후 nums[i]로 나누는 방식은 불가

 

2. 요구사항 분석

  • 입력
    • 2 <= nums.length <= 100,000
    • -30 <= nums[i] <= 30
  • 출력
    • answer[i] 배열

 

3. 해결 아이디어

  1. 왼쪽 누적곱(prefix)와 오른쪽 누적곱(suffix)을 각각 저장
  2. answer[i] = 왼쪽 누적곱 * 오른쪽 누적곱

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도 
    • 두 번의 배열 순회가 필요하므로 O(n)
  • 공간 복잡도
    • answer 배열이 필요하므로 O(n)

5. 구현

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        // 1. 정답을 저장할 배열 할당
        int[] answer = new int[n];

        // 2. 왼쪽 누적곱을 계산하여 answer에 저장
        answer[0] = 1;
        for (int i = 1; i < n; i++) {
            answer[i] = answer[i - 1] * nums[i - 1];
        }

        // 3. 오른쪽 누적곱을 저장하면서 answer와 곱함
        int right = 1;
        for (int i = n - 1; i >= 0; i--) {
            answer[i] *= right;
            right *= nums[i];
        }

        return answer;
    }
}

 

랜덤Set 만들기

 

예제 - LeetCode 380. Insert Delete GetRandom O(1)

https://leetcode.com/problems/insert-delete-getrandom-o1/description/?envType=study-plan-v2&envId=top-interview-150

 

1. 문제 이해

  • 삽입, 삭제, 랜덤 조회 연산의 시간 복잡도가 O(1)인 RandomizedSet 만들기

 

2. 요구사항 분석

  • 입력
    • val은 int 범위
    • 각각의 메소드는 200,000번 이상 호출됨
    • getRandom 메서드는 최소 한개 이상의 요소가 있을 때 호출됨
  • 출력
    • 메서드 결과

 

3. 해결 아이디어

  1. ArrayList : 인덱스를 통해 O(1) 랜덤 접근 가능
  2. HashMap : 원소에 해당하는 인덱스를 매핑하여 O(1) 접근 가능
  3. 삭제 시에는 마지막 원소와 교체 후 pop함으로 O(1) 수행 가능

 

4. 시간 복잡도 및 공간 복잡도 확인

 

  • 시간 복잡도 
    • 리스트와 해시맵을 활용하여 해당 값에 바로 접근 가능하므로 O(1)
  • 공간 복잡도
    • n크기의 배열과 맵이 필요하므로 O(n)

5. 구현

public class RandomizedSet {
    // 1. 리스트, 맵, 랜덤 초기화 
    private List<Integer> list;
    private Map<Integer, Integer> map;
    private Random rand;

    public RandomizedSet() {
        list = new ArrayList<>();
        map = new HashMap<>();
        rand = new Random();
    }

    // 2. 삽입
    public boolean insert(int val) {
        // 2-1) containsKey를 사용하여 중복을 바로 확인
        if (map.containsKey(val)) return false;

        // 2-2) 중복이 아닌 경우 맵과 리스트에 값, 인덱스 저장
        map.put(val, list.size());
        list.add(val);
        return true;
    }

    // 3. 삭제
    public boolean remove(int val) {
        // 3-1) containsKey를 사용하여 중복을 바로 확인
        if (!map.containsKey(val)) return false;

        // 3-2) 맵을 통해 해당 요소의 인덱스를 바로 확인
        int index = map.get(val);

        // 3-3) 마지막 요소와 해당 요소를 교체 후 제거
        int lastElement = list.get(list.size() - 1);

        list.set(index, lastElement);
        map.put(lastElement, index);

        // 3-4) 마지막 요소 제거
        list.remove(list.size() - 1);
        map.remove(val);

        return true;
    }

    // 4. 랜덤 조회
    public int getRandom() {
        // 4-1) 리스트의 인덱스 중 랜덤한 값을 선택 후 해당 요소 반환
        int index = rand.nextInt(list.size());
        return list.get(index);
    }
}