리스트 (List)
리스트(List)란 순서를 가지고 원소를 저장하는 자료구조로 Array List나 Linked List로 구현이 가능하다.
- Array List는 요소를 빈번히 검색해야 하거나, 중간 삽입과 삭제가 적은 경우에 적합하다.
- Linked List는 삽입과 삭제가 빈번하고, 데이터의 물리적 위치가 중요하지 않은 경우에 유리하다.
Array List
배열을 기반으로 구성된 List형 자료구조로 정적배열(Static Array)이나 동적배열(Dynamic Array)로 구현이 가능하다.
💡 Static Array
Static Array는 다음과 같은 두 가지 특징을 가진다.
- 고정된 크기의 저장 공간을 가진다.
- 연속된 메모리에 순차적으로 데이터를 저장한다.
즉, 배열을 선언할 때 선언한 크기만큼 연속된 메모리를 할당 받아 연속적으로 데이터를 저장한다.
📍 Random access
메모리에 저장된 데이터에 접근하려면 해당 데이터의 주소값을 알아야 한다.
배열은 연속적이므로 첫 번째 주소값만 알고 있다면 어떤 인덱스에도 즉시 접근이 가능하다.
이를 Direct access 또는 Random access 라고 부른다.
📍 Static Array 선언
int[] intArray = new int[10];
String[] stringArray = new String[5];
char[] charArray = new char[]{ 'h', 'e', 'l', 'l', 'o' };
char[] charArray = { 'h', 'e', 'l', 'l', 'o' };
※ 배열을 초기화하고 값을 넣어주지 않으면 기본값으로 설정된다.
int = 0 , char = \u0000 , string = null
📍 Static Array 접근 및 수정
intArray[i]; // i번째 원소에 접근
intArray[intArray.length - 1]; // 마지막 원소에 접근
intArray[i] = 3; // i번째 원소를 3으로 수정
📍 Static Array 복사
// 1차원 배열 복사
int[] arr = { 1, 2, 3, 4, 5 };
int[] copy = Arrays.copyOf(arr, arr.length); // 전체 복사 copy = { 1, 2, 3, 4, 5 }
int[] copy = Arrays.copyOf(arr, 2, 5); // 특정 부분 복사 copy = { 3, 4, 5 }
// 2차원 배열 복사
int[][] arr = { ... };
int[][] copy = new int[arr.length][arr[0].length];
for(int i = 0; i < arr.length; i++) {
copy[i] = Array.copyOf(arr[i], arr[0].length);
}
Arrays.copyOf( ) 메소드를 사용하여 2차원 배열을 복사할 경우 shallow copy가 발생할 수 있다.
deep copy를 하려면 for 문을 사용하여 각 열에 대한 복사가 필요하다.
※ Shallow Copy
내부 배열들은 복사되는 않는 것으로 복사된 배열을 변경하면 원본 배열이 함께 수정되는 현상이다.
int[][] arr = { { 1, 2, 3 } , { 4, 5, 6 } } int[][] copy = Array.copyOf(arr, arr.length); copy[0][0] = 9999; // 복사본 수정 System.out.println("arr[0][0] = " + arr[0][0]); System.out.println("copy[0][0] = " + copy[0][0]);
// 결과 arr[0][0] = 9999 copy[0][0] = 9999
📍 Static Array 정렬
// 오름차순으로 정렬
int[] arr = { 1, 3, 2 };
Arrays.sort(arr);
for(int num : arr) {
System.out.println("num = " + num);
}
// 결과
num = 1
num = 2
num = 3
📍 Static Array 초기화
int[] arr = new int[10];
Arrays.fill(arr, 5); // 모든 인덱스가 5로 초기화
💡 Dynamic Array
Dynamic Array는 다음과 같은 두 가지 특징을 가진다.
- 유동적인 크기의 저장 공간을 가진다.
- 필요에 따라 크기를 확장하고 데이터를 저장한다.
크기 변경이 불가능한 정적 배열과 달리 동적 배열에서는 크기 변경이 가능하므로 코딩 테스트에서 자주 사용된다.
📍 resize
동적 배열에 데이터를 추가하다 할당된 크기를 초과하면, 크기를 늘린 배열을 새로 선언하고 데이터를 옮긴다.
그리고 기존에 선언된 배열은 메로리에서 삭제하는데 이 과정을 resize 라고 한다.
resize를 한 칸씩 늘린다면 비효율적이므로 보통 2배 큰 크기로 resize하는데 이를 doubling 이라고 한다.
📍 Dynamic Array 사용
List 인터페이스와 ArrayList 클래스를 사용하면 직접 구현할 필요 없이 Dynamic Array를 사용할 수 있다.
우리가 알아야 할 것은 ArrayList 연산들과 시간 복잡도이다.
Static Array | Dynamic Array | |
배열에 접근 및 수정 | $O(1)$ | $O(1)$ |
배열의 끝에 새로운 요소 삽입 | $O(1)$ | $O(1)$ |
배열의 마지막 요소 삭제 | $O(1)$ | $O(1)$ |
배열의 특정 위치에 새로운 요소 삽입 | $O(n)$ | $O(n)$ |
배열의 특정 위치의 요소 삭제 | $O(n)$ | $O(n)$ |
📍 Array List 선언
// Integer 타입을 저장하는 list라는 이름을 가진ArrayList 선언
List<Integer> list = new ArrayList<>();
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
// Integer 타입을 저장하는 list라는 이름의 ArrayList 선언과 동시에 초기화
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
📍 Array List 접근 및 수정
list.get(i); // i번째 원소에
list.set(i, 3); // i번째 원소를 3으로 수정
📍 Array List 추가
Array List에 원소를 추가하는 경우, 마지막에 원소를 삽입하는 경우와 중간에 원소를 삽입하는 경우로 나눌 수 있다.
마지막에 원소를 삽입하는 경우, 맨 뒤에 원소를 추가하기만 하면 되므로 $O(1)$의 시간 복잡도를 갖는다.
하지만 중간에 원소를 삽입하는 경우, 원소를 삽입한 후 뒤의 원소들을 한 칸씩 미루어야 하므로 $O(n)$이다.
list.add(1); // 마지막에 원소 1 추가
list.add(2, 10); // 2번째 위치에 원소 10 추가
📍 Array List 삭제
Array List의 원소를 삭제하는 경우도 마지막 원소를 삭제하는 경우와 중간 원소를 삭제하는 것으로 나눌 수 있다.
마지막 원소를 삭제하는 경우, 맨 뒤의 원소를 삭제하기만 하면 되므로 $O(1)$의 시간 복잡도를 갖는다.
하지만 중간의 원소를 삭제하는 경우, 원소를 삭제한 후 뒤의 원소들을 한 칸씩 당겨야 하므로 $O(n)$이다.
list.remove(list.size()-1)); // 마지막 원소 삭제
list.remove(2); // 2번째 위치의 원소 삭제
list.remove(Integer.valueIf(2)); // 2값을 갖는 원소 중 제일 앞에 있는 값 삭제
※ ArrayList<Integer> 의 remove( ) 사용할 때 주의점
remove( ) 인자로 인덱스와 지울 오브젝트를 받는 두 가지 오버라이드가 존재한다.
때문에 원소값이 2인 요소를 지우려고 list.remove(2) 를 사용하면 2번째 인덱스 값이 삭제된다.
따라서 값이 2인 요소를 삭제하려면 list.remove(Integer.valueOf(2)) 를 사용해야 한다.
단, 이는 정수 리스트에만 해당하는 것으로 스트링 리스트라면 list.remove("a") 로 사용해도 무방하다.
📍 Array List 정렬
List<Integer> arrayList = new ArrayList<>(List.of(1, 3, 2, 5, 4));
arrayList.sort(Integer::compareTo); // 오름차순 정렬
arrayList.sort(Comparator.reverseOrder()); // 내림차순 정렬
📍 2차원 Array List
배열과 다르게 리스트는 크기가 초기화 되어 있지 않으므로 새로운 행을 추가하며 사용해야 한다.
List<List<Integer>> doubleList = new ArrayList<>();
for(int i=0; i<10; i++) {
List<Integer> newRow = new ArrayList<>(); // 새로운 열 추가
for(int j=1; j<10; j++) {
newRow.add(i*10 + j);
}
doubleList.add(newRow);
}
// 결과
[[1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10],
[11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
...]
※ 2차원 Array List의 deep copy & shallow copy
2차원 Array List도 일반적인 방식으로 복제하면 shallow copy가 되어 원본에 영향을 준다.
List<List<Integer>> doubleList = new ArrayList<>(); List<List<Integer>> copyList = new ArrayList<>(doubleList); copyList.get(0).set(0, 9999);.
// doubleList 결과 [[9999, 2, 3, 4, 5, 6, 7, 8, 9, 10], ...]
때문에 2차원 Static Array와 마찬가지로 2차원 Array List도 for 문을 이용하여 각 행을 따로 복제해야 한다.
List<List<Integer>> doubleList = new ArrayList<>(); List<List<Integer>> copyList = new ArrayList<>(); for(List<Integer> row : doubleList) { copyList.add(new ArrayList<>(row)} } copyList.get(0).set(0, 9999);
// doubleList 결과 [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ...]
'Algorithm > 개념' 카테고리의 다른 글
[Algorithm] 06. 스택 (Stack) (0) | 2024.08.07 |
---|---|
[Algorithm] 05. 큐 (Queue) (0) | 2024.08.07 |
[Algorithm] 04. 해시테이블 (Hash Table) (0) | 2024.08.05 |
[Algorithm] 03. 정렬 (Sorting) (0) | 2024.08.02 |
[Algorithm] 01. 알고리즘(Algorithm) 개념 및 평가 기준 (0) | 2024.07.31 |