읽고 나면 진짜 쉬워지는 자료 구조 - 1. 메모리 안 정보
데이터를 메모리에 저장하는 기본 방법
🔖 1.1. 변수
변수는 라벨이다
변수는 종이 문서를 담는 폴더에 붙은 종이 라벨과 같다. 라벨을 붙인 후에는 폴더의 순서나 위치를 기억할 필요가 없다.
따라서 충분한 정보가 포함된 이름을 사용하는 것이 중요하다.
변수는 저장된 데이터의 타입과 연관이 있기도 하다.
🔖 1.2 복합 자료 구조
복잡하니까 묶어야지
복합 자료 구조는 여러 개별 변수를 한 그룹으로 엮은 구조체나 객체 등을 말한다.
명함은 이름, 이메일 등과 같은 여러 정보를 포함하는 데이터 꾸러미다. (좋은 비유!)
🔖 1.3 배열
배열만의 특징
배열 내 상자들은 컴퓨터 메모리에서 서로 인접해 있으므로, 첫 번째 원소로부터 offset을 계산해 해당하는 위치의 메모리를 읽는 방식으로 각 상자에 쉽게 접근할 수 있다.
이는 접근하려는 상자의 위치와 관계 없이 덧셈 한 번과 메모리 접근만 필요하다는 뜻이다.
그래서 배열은 순서가 있는 항목을 저장할 때 특히 편하다.
삽입 정렬은 배열의 일부를 정렬하고, 이 정렬된 범위를 전체 배열이 정렬될 때까지 확장한다. 알고리즘은 정렬되지 않은 배열의 각 원소를 반복하면서 정렬된 부분의 올바른 위치로 이동한다.
책 내용과 연결짓기
인덱스를 1부터 시작하는 것도 가능하며, 일부 프로그래밍 언어는 이 규칙을 따른다고 한다. 새롭게 알게 된 사실! 대표적으로 R이 벡터나 리스트의 인덱스가 1부터 시작한다고 한다. 다만, C, Python, JavaScript 같은 언어는 0부터 시작하는 걸 기본으로 사용한다. 이는 메모리 주소 기반으로 배열이 설계된 컴퓨터 공학적 전통에서 비롯된 것이라 한다.
책에서 배열은 책장처럼 책을 끼워넣을 수 있는 게 아니라, 일렬로 늘어선 가게와 같다고 말한다. 그래서 때로는 배열이 비효울적인 자료구조라고 말하는 거겠지?
삽입 정렬
삽입 정렬은 배열의 일부를 정렬하고, 이 정렬된 범위를 전체 배열이 정렬될 때까지 확장한다. 즉, 삽입 정렬은 처음에는 작은 정렬된 부분을 만들고, 새로운 요소를 하나씩 추가하면서 이 정렬된 범위를 점점 확장해 나간다. 이렇게 해서 결국 배열 전체가 정렬된다.
따라서 i의 반복을 시작하는 시점에 i-1 이하의 위치에 있는 원소는 모두 정렬되어 있다.
삽입 정렬과 선입 선출
책에서 커피를 신선도 순으로 정렬하는 것을 예시로 들었다. 그렇다면 선입선출(FIFO)과 다른 점은 무엇인가?
삽입 정렬은 단순히 들어온 순서대로 정렬하는 것이 아니라, 새로운 상품이 들어올 때마다 이미 정렬된 구간 속에서 알맞은 자리를 찾아 넣어 전체적으로 "유통기한이 빠른 순서"를 유지하려는 전략에 가깝다.
정리하자면, 선입선출은 들어온 순서를 그대로 따르는 것이고, 삽입 정렬은 새로운 요소를 삽입할 때마다 기준에 맞게 위치를 재조정하는 정렬 방식이다.
중첩된 루프로 삽입 정렬 구현
function insertionSort(A: number[]) {
const N = A.length;
let i = 1; // 0은 이미 정렬된 상태로 가정
while (i < N) {
let current = A[i]; // 삽입하려는 요소를 임시로 저장
let j = i - 1; // j는 현재 요소 바로 앞 요소부터 비교 시작
while (j >= 0 && A[j] > current) {
A[j + 1] = A[j]; // j번째 요소를 뒤로 한 칸 이동
j = j - 1; // j를 한 칸 앞으로 이동 (더 앞 요소와 비교 위해)
}
A[j + 1] = current; // 이동이 끝난 위치에 current 배치
i = i + 1; // 다음 위치 작업을 위해 i 증가
}
return A;
}
시간 복잡도
삽입 정렬의 시간 복잡도는 평균적으로 O(N²)다.
이미 거의 정렬되어 있는 상태라면 최선의 경우는 O(N) 정도로 더 빨라질 수 있지만.. 최악의 경우와 평균적인 경우에 대략 N²번 정도의 비교와 이동 연산을 수행한다.
이처럼 그다지 효율적이지 않지만, 책에서도 말하듯 배열을 다루는 방식을 이해하는 데 도움을 주는 정렬 알고리즘이다.
삽입 정렬을 통해 알아본 배열의 특징은 세 가지로 정리할 수 있다. 인덱스로 원소 접근하는 것, 새 원소 삽입 시 값을 교환한다는 것, 모든 원소를 반복할 수 있어야 한다는 것.
🔖 1.4 문자열
문자열과 배열의 연관성
문자열은 종종 특수한 종류의 배열로 생각할 수 있는, 순서가 지정된 문자의 리스트다.
문자열은 내부적으로 문자들이 순서대로 나열된 구조를 지니고 있기 때문에 "문자들의 배열" 이라 볼 수 있다. (물론 실제 구현이나 특성은 다름)
정리하자면,
str[0]
와arr[0]
모두 "인덱스를 이용해 순서 있는 데이터에 접근한다"는 면에서 비슷하지만, 이 둘을 동일하다고 하기엔 자료형과 언어에 따른 구현상 차이가 있다.
const arr = [1, 2, 3, 4];
const str = 1234;
console.log(arr[1]); // 2
console.log(str[1]); // undefined
두 문자열의 동등성 확인 알고리즘
function stringEqual(str1: string, str2: string): boolean {
// 두 문자열의 길이가 다르면 바로 false return
if (str1.length !== str2.length) {
return false;
}
// 길이가 같다면 각 문자를 비교
for (let i = 0; i < str1.length; i++) {
// 하나라도 다른 문자가 있으면 false return
if (str1[i] !== str2[i]) {
return false;
}
}
// 모든 문자가 같다면 true return
return true;
}
시간 복잡도
최선의 경우, 첫 문자가 다르면 한 번의 비교로 끝나므로 O(1)다.
하지만 평균적으로 또는 최악의 경우. 모든 문자를 비교해야 하므로 O(N)이다.
배열 vs. 문자열
그렇다면 알고리즘 문제를 해결 할 때, 배열과 문자열 둘 중 어떤 쪽이 더 효율적일까? 문제의 상황에 따라 적절한 자료구조를 선택하면 될듯 하다.
구분
배열(Array)
문자열(String)
장점
• 임의 접근 (O(1)): 인덱스를 사용해 원하는 위치의 요소에 즉시 접근 가능 • 유연성: 다양한 데이터 타입 저장 가능 • 다양한 연산 지원: 삽입, 삭제, 정렬 등 다양한 알고리즘 효율적 구현 가능
• 특수한 연산 지원: 서브스트링, 분할, 결합 등 문자열 전용 함수 제공 • 불변성: 안전하게 공유 가능 • 텍스트 처리 최적화: 패턴 매칭, 정규 표현식 등 텍스트 처리에 유리
단점
• 고정 크기: 일부 언어에서는 크기 고정, 변경 시 새로운 배열 생성 필요 • 메모리 사용: 연속된 메모리 필요, 큰 배열은 메모리 낭비 가능
• 수정의 어려움: 불변인 경우 수정 시 새로운 문자열 생성 필요 • 제한된 데이터 타입: 주로 문자만 저장 가능, 다양한 타입 저장 불가
TC
• 접근 및 수정: 임의 접근 및 수정이 빠름 (O(1)) • 삽입/삭제: 중간에 삽입/삭제 시 O(N) 소요
• 접근: 인덱스 접근은 O(1)이지만, 수정 시 전체 문자열 복사 필요 (O(N)) • 검색/패턴 매칭: 최적화된 알고리즘 사용 시 효율적 (일반적으로 O(N))
효율성
• 데이터 조작: 정렬, 삽입, 삭제 등 일반적인 데이터 조작에 유리 • 다양한 타입: 여러 데이터 타입을 다뤄야 할 때 적합
• 텍스트 처리: 검색, 패턴 매칭 등 텍스트 관련 작업에 최적화 • 문자열 전용 함수 활용: 문자열 관련 기능을 쉽게 구현 가능
메모리
• 유연한 타입 저장: 다양한 타입 저장 가능, 메모리 사용이 유동적 • 연속 메모리: 큰 배열은 메모리 낭비 우려
• 고정된 문자 타입: 문자만 저장, 메모리 사용이 일정 • 불변성: 문자열 변경 시 추가 메모리 사용
📚 관련 문제와 풀이
Last updated