01. 배열
- 인덱스와 값을 일대일 대응해 관리하는 자료구조
- 데이터를 저장할 수 있는 모든 공간은 인덱스와 일대일 대응하므로 어떤 위치에 있는 데이터든 한 번에 접근 가능
배열 선언
int[] arr = { 0, 0, 0, 0, 0, 0 };
int[] arr = new int[6];
// 둘 실행 결과 같음
- 배열은 인덱스가 0부터 시작
- 배열과 유사한 기능을 가진 ArrayList와 차이
- 배열: 처음 선언할 때 배열의 크기 결정
- 정확한 데이터의 개수를 알고 있는 경우 (속도 더 빠름)
- ArrayList: 크기 동적임
- 저장해야 할 데이터의 개수를 정확히 알 수 없는 경우
- 배열: 처음 선언할 때 배열의 크기 결정
배열과 차원
- 컴퓨터 메모리의 구조는 1차원이므로 2차원, 3차원 배열도 실제로는 1차원 공간에 저장
- 배열은 차원과는 무관하게 메모리에 연속 할당
- 1차원 배열
- 배열의 각 데이터는 메모리의 낮은 주소에서 높은 주소 방향으로 연이어 할당
- 2차원 배열
- 0행, 1행 순서로 데이터를 할당해 1차원 공간에 저장
int[][] arr = {{1,2,3},{4,5,6}};
02. ArrayList
ArrayList<Integer> list1 = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 다른 컬렉션의 데이터로 초기화
ArrayList<Integer> list2 = new ArrayList<>(list1);
// get() 메서드를 통해 데이터 접근
System.out.println(list1.get(0)); // 1
// remove() 메서드로 데이터 삭제
// 데이터를 삭제하는 위치에 따라 데이터를 복사하는 연산이 필요하므로
// 시간 복잡도가 O(N)까지 증가할 수 있으므로 주의 필요
list.remove(list.size() - 1); // 끝에 있는 데이터 삭제
(+) 배열, ArrayList 메서드
int[] arr = {1, 2, 3, 4, 5};
System.out.println(arr.length); // 5
Arrays.sort(arr); // 정렬
System.out.println(Arrays.toString(arr)); // 출력
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1,2,4,5,3));
System.out.println(list.size());// 5
System.out.println(list.isEmpty());// false
Collections.sort(list); // 정렬
System.out.println(list) // 출력
ArrayList 효율성
- 배열은 임의 접근이라는 방법으로 배열의 모든 위치에 있는 데이터에 단번에 접근 가능
- 데이터에 접근하기 위한 시간 복잡도 = O(1)
- 데이터 추가 연산에 대한 시간 복잡도
- 맨 뒤에 삽입
- 데이터가 가득찬 상태에서 추가로 데이터를 또 삽입하면 리사이징(배열의 크기 늘림)
- 50% 증가 전략 사용
- 리사이징할 때마다 새 배열을 생성하는 것이므로 기존 데이터를 복사하는 과정 필요
- O(N)
- 리사이징을 자주하면 효율이 좋지 않음
- 그렇다고 기본 사이즈를 크게 하거나 리사이징시 더 큰 크기의 배열을 생성하면 낭비 공간이 많아져 메모리 효율이 좋지 않음
- 50% 증가 전략
- ArrayList의 사이즈가 작을 때는 리사이징이 자주 발생하는 대신 데이터의 개수가 적어 새로운 메모리 영역에 복사하는 시간이 빠름
- 사이즈가 클 때는 리사이징 시에 늘어나느 공간이므로 리사이징이 자주 발생하지 않는 대신 복사해야 할 데이터의 수가 많음
- 코딩 테스트에서는 ArrayList의 리사이징으로 인한 효율성 이슈는 고려하지 않아도 될 정도로 비용이 상당히 적으니 무시해도 됨
- 데이터가 가득찬 상태에서 추가로 데이터를 또 삽입하면 리사이징(배열의 크기 늘림)
- 맨 앞에 삽입
- 기존 데이터들을 뒤로 한 칸씩 밀어야 하므로 O(N)
- 중간에 삽입
- 현재 삽입한 데이터 뒤에 있는 데이터 개수만큼 미는 연산 → O(N)
- 맨 뒤에 삽입
배열 선택시 고려사항
- 데이터에 자주 접근하거나 읽어야 하는 경우 배열을 사용하면 좋은 성능 낼 수 있음
- 메모리 공간을 충분히 확보해야 한다는 단점
- 임의 접근이라는 특징이 있어 데이터에 인덱스로 바로 접근할 수 있어 데이터에 빈번하게 접근하는 경우 효율적이지만 메모리 낭비가 발생할 수 있음
- 고려사항
- 할당할 수 있는 메모리 크기를 확인해야 함
- 배열로 표현하려는 데이터가 너무 많으면 런타임에서 배열할당에 실패할 수 있음
- 코딩 테스트에서는 정수형 1차원 배열은 1000만개, 2차원 배열은 3000*3000 크기를 최대로 생각해야 함
- 중간에 데이터 삽입이 많은지 확인해야 함
- 배열은 선형 자료구조이기 때문에 중간이나 처음에 데이터를 빈번하게 삽입하면 시간 복잡도가 높아져 실제 시험에서 시간 초과가 발생할 수 있음
- 할당할 수 있는 메모리 크기를 확인해야 함
배열 몸풀기 문제
01. 배열 정렬하기
Arrays.sort(arr);
return arr;
- Arrays.sort() 메서드
- 배열을 오름차순으로 정렬
- O(NlogN)
- sort() 메서드는 원본 배열 자체를 정렬시킴
- 원본 배열을 그대로 두고 싶은 경우
- clone() 메서드
- 타겟이 되는 배열을 복사하여 새로운 배열로 생성
- clone() 메서드
- int[] clone = arr.clone(); Arrays.sort(clone); return clone;
- 원본 배열의 상태를 유지하면서 원본 배열로부터 새로운 배열을 복사해서 사용해야 하는 상황에서는 clone() 메서드를 사용해야 함
02. 배열 제어하기
정수 배열의 중복 값을 제거하고 배열 데이터를 내림차순으로 정렬해서 반환
Integer[] result = Arrays.stream(arr).boxed().distinct().toArray(Integer[]::new);
Arrays.sort(result, Collections.reverseOrder());
return Arrays.stream(result).mapToInt(Integer::intValue).toArray();
- Arrays.stream(arr).boxed().distinct().toArray(Integer[]::new)
- stream으로 변환
- boxed 메서드를 통해 IntStream → Stream<Integer>
- distinct()가 객체 비교를 해야 하므로 필요
- distinct() 메서드를 통해 중복을 제거
- O(N) 보장
- toArray(Integer[]::new) → 결과를 Integer[] 타입 배열로 변환
- Arrays.sort(result, Collections.reverseOrder());
- sort 내림차순 정렬 → O(NlogN)
- 최종 시간 복잡도 → O(NlogN)
03. 두 개 뽑아서 더하기
정수 배열 numbers가 주어집니다. numbers에서 서로 다른 인덱스에 있는 2개의 수를 뽑아 더해 만들 수 있는 모든 수를 배열에 오름차순으로 담아 반환하는 solution() 함수를 완성하세요.
HashSet<Integer> set = new HashSet<>();
for (int i = 0; i < numbers.length - 1; i++) {
for (int j = i + 1; j < numbers.length; j++) {
set.add(numbers[i] + numbers[j]);
}
}
return set.stream().sorted().mapToInt(Integer::intValue).toArray();
- HashSet → 중복 값을 제거한 값을 저장