
배열(Array): 가장 빠르고, 가장 딱딱한 자료구조 (완전정복)
기차 좌석처럼 연속된 메모리를 쓰는 배열. O(1) 조회 속도의 비밀인 주소 계산 공식부터, CPU 캐시 지역성(Cache Locality), 버퍼 오버플로우 보안 이슈, 그리고 동적 배열의 내부 구현까지.

기차 좌석처럼 연속된 메모리를 쓰는 배열. O(1) 조회 속도의 비밀인 주소 계산 공식부터, CPU 캐시 지역성(Cache Locality), 버퍼 오버플로우 보안 이슈, 그리고 동적 배열의 내부 구현까지.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

자료구조의 조상님이자, 모든 개발자가 가장 먼저 배우는 배열(Array)입니다. 단순해 보이지만, 이 녀석의 "메모리 연속성(Contiguous Memory)"이라는 특성을 완벽히 이해해야 고성능 애플리케이션을 만들 수 있습니다.
배열은 메모리상의 연속된 땅을 미리 분양받는 것과 같습니다. "나 여기서부터 저기까지 100평 쓸 거야! 아무도 건들지 마!"
배열이 사랑받는 유일하고도 강력한 이유는 조회 속도가 깡패라는 점입니다. 데이터가 100만 개가 있어도, 100만 번째 데이터를 찾는 속도는 1번째 데이터를 찾는 속도와 똑같이 즉시(Instant)입니다. 이것이 가능한 이유는 "탐색"을 하지 않고 "계산"을 하기 때문입니다.
컴퓨터는 배열의 시작 주소만 알면, 수학 공식 하나로 원하는 위치를 즉시 계산합니다.
공식:
Target Address = Start Address + (Index × Data Size)
예를 들어, int arr[5]가 메모리 주소 1000번지에서 시작한다고 가정합시다. (int는 4바이트)
arr[0] -> 1000 + (0 × 4) = 1000번지arr[1] -> 1000 + (1 × 4) = 1004번지arr[2] -> 1000 + (2 × 4) = 1008번지arr[999] -> 1000 + (999 × 4) = 4996번지역무원이 5호차 좌석을 찾을 때 1호차부터 걸어가지 않는 것과 같습니다. "한 칸에 20미터니까 5호차는 100미터 지점이네" 하고 순간 이동하는 것과 같습니다. 이것이 바로 랜덤 액세스(Random Access)의 위력입니다.
하지만 빈틈없는 연속성은 치명적인 약점이 됩니다. 중간에 누가 끼어드는 꼴을 용납하지 못합니다.
영화관 좌석이 꽉 차 있는데, 갑자기 3번째 자리에 친구를 앉히고 싶습니다.
데이터가 100만 개라면? 100만 번의 이동 연산이 발생합니다.
push): 빠름 (O(1)) - 그냥 뒤에 의자 하나 놓으면 됨.insert): 개느림 (O(N)) - 전원 이동.delete): 마찬가지로 빈자리를 채우기 위해 전원이 당겨 앉아야 함 (O(N)).그래서 데이터의 추가/삭제가 빈번한 게시판 댓글이나 채팅 로그 같은 곳에는 배열을 쓰면 안 됩니다. (이럴 땐 연결 리스트를 씁니다.)
실무에서 가장 중요한 고난이도 질문입니다. "이론상으로는 연결 리스트(Linked List)가 삽입/삭제가 O(1)이라 빠른데, 왜 실제로는 배열(Vector)을 많이 쓰나요?"
정답은 CPU 캐시(Cache) 때문입니다. CPU는 메모리(RAM)에서 데이터를 가져올 때, 달랑 하나만 가져오지 않습니다. RAM은 너무 느리기 때문이죠. "어차피 주인님이 옆에 있는 데이터도 찾겠지?"라고 예측하고 주변 한 덩어리(Cache Line, 보통 64바이트)를 미리 다 퍼옵니다(Prefetching).
[Data1][Data2][Data3][Data4]...
-> 붙어 있으니까 한 번에 캐시로 로딩됨!
-> Data2 부를 때 이미 캐시에 있음 (Cache Hit)
-> 초고속 처리
Linked List (슬픈 CPU)
[Data1] ----> (저 멀리 산너머) [Data2] ----> (강 건너) [Data3]
-> 흩어져 있음.
-> Data2 부를 때마다 RAM에 다시 다녀와야 함 (Cache Miss)
-> CPU가 멍때리는 시간 발생 (Stall)
이 차이 때문에 실제 벤치마크를 돌려보면 배열이 연결 리스트보다 수십 배에서 수백 배 빠를 수 있습니다. 하드웨어 친화적인 코딩이 무엇인지 보여주는 가장 좋은 예시입니다.
배열은 해커들이 가장 좋아하는 먹잇감입니다. 특히 C/C++ 처럼 메모리를 직접 다루는 언어에서, 경계 검사(Boundary Check)를 하지 않을 때 발생합니다.
char buffer[8]을 선언했습니다. (8칸짜리 배열)최신 OS는 이를 막기 위해 카나리아(Canary) 값 심기, ASLR 등 온갖 방어 기법을 쓰지만, 근본적인 해결책은 Java, Python, Rust 같이 인덱스 검사를 강제하는 언어를 쓰는 것입니다.
"파이썬 리스트나 자바 ArrayList는 크기가 무한대잖아요? 배열 아니라면서요?" 아닙니다. 걔네들도 내부적으로는 고정 크기 배열을 씁니다. 다만 "이사(Migration)"를 잘할 뿐입니다.
이 과정은 O(N) 비용이 듭니다. 하지만 아주 가끔 일어나므로, 평균을 내보면(Amortized Analysis) 여전히 O(1)입니다.
Tip: 여러분이 만약 담을 데이터 개수를 대충 안다면(예: 100만 개), new ArrayList<>(1000000) 처럼 초기 크기를 지정해주세요. 이사 비용을 아껴서 성능을 획기적으로 높일 수 있습니다.
2차원 배열 arr[100][100]을 반복문으로 돌 때, 어떤 순서가 빠를까요?
// A방식: 행 우선 (Row-Major)
for(int r=0; r<100; r++)
for(int c=0; c<100; c++) arr[r][c]...
// B방식: 열 우선 (Column-Major)
for(int c=0; c<100; c++)
for(int r=0; r<100; r++) arr[r][c]...
C/C++/Java/C#은 행 우선(Row-Major) 언어입니다. 메모리에 [0,0], [0,1], [0,2]... 순서로 저장됩니다.
그래서 A방식이 캐시 히트율이 높아 훨씬 빠릅니다. B방식은 캐시를 계속 엇나가게(Cache Miss) 만듭니다.
반대로 Fortran, MATLAB, R, Julia 같은 과학 계산용 언어는 열 우선(Column-Major)입니다. 여기서는 B방식이 빠릅니다. 데이터 분석가들이 Python(행 우선)으로 넘어올 때 가장 많이 하는 실수입니다.
우리가 흔히 "배열이 빠르다"고 할 때, 그 "빠름"의 근원이 도대체 어디서 오는지 물리 레벨에서 파헤쳐봤습니다. 이 내용을 알면 왜 개발자가 하드웨어를 이해해야 하는지 알게 됩니다.
우리가 쓰는 메인 메모리(RAM)는 대부분 DRAM (Dynamic RAM)입니다. DRAM의 각 비트(0 또는 1)는 커패시터(Capacitor)라는 아주 작은물통에 전기를 채우는 방식으로 저장됩니다. 문제는 이 물통에 구멍이 뚫려있다는 것입니다. 가만히 놔두면 전기가 줄줄 샙니다(Leakage). 그래서 컴퓨터는 1초에 수십 번씩 전기를 다시 채워주는 작업을 합니다. 이것을 리프레시(Refresh)라고 합니다. 이 리프레시 작업 때문에 CPU가 메모리에 접근하려고 할 때 "잠깐만! 지금 물 채우는 중이야!"라며 기다려야 하는 상황이 발생합니다. 배열을 쓰면 데이터를 연속적으로 읽기 때문에, 메모리 컨트롤러가 "어차피 계속 읽을 거니까 미리미리 준비해두자"는 최적화를 할 수 있습니다.
CPU 바로 옆에 붙어있는 캐시 메모리는 SRAM (Static RAM)입니다. 얘는 커패시터(물통)가 아니라 플립플롭(Flip-Flop)이라는 회로를 씁니다. 전기가 새지 않아서 리프레시가 필요 없고, 속도가 빛의 속도에 가깝습니다. 하지만 트랜지스터가 6개나 들어가서 덩치가 크고 비쌉니다. 그래서 용량을 작게 만듭니다.
배열을 사용한다는 것은, 이 비싸고 귀하신 SRAM(캐시)을 100% 활용하겠다는 서약과 같습니다. 반면 연결 리스트를 쓰면, 비싼 SRAM을 놔두고 느려터진 DRAM(메인 메모리)까지 매번 다녀와야 합니다. 이것은 마치 최고급 스포츠카(CPU)를 타고 꽉 막힌 시골길(DRAM 접근)을 달리는 것과 같습니다. 배열을 쓰는 것은 스포츠카를 위해 고속도로(Sequential Access)를 뚫어주는 행위입니다.
운영체제는 메모리를 페이지(Page, 보통 4KB) 단위로 관리합니다. 배열은 데이터가 옹기종기 모여 있어서, 페이지 하나만 메모리에 올리면 수천 개의 데이터를 처리할 수 있습니다. 하지만 연결 리스트는 노드 하나가 여기 있고, 다음 노드는 저기 멀리 다른 페이지에 있을 수 있습니다. 재수 없으면 노드 하나 읽을 때마다 하드디스크에서 페이지를 새로 로딩해야 하는 페이지 폴트(Page Fault)가 발생합니다. 페이지 폴트는 CPU 입장에서는 거의 영겁의 시간(수 밀리초) 동안 멈춰 있어야 하는 대재앙입니다. 대용량 처리를 할 때 배열이 압도적인 이유가 바로 이 OS 페이지 관리 효율성 때문입니다.
배열이 왜 AI(인공지능) 시대의 주인공인지 아시나요? 바로 SIMD (Single Instruction, Multiple Data) 기술 때문입니다.
일반적인 코드는 이렇게 동작합니다.
a[0] + b[0] 계산 (1 사이클)
a[1] + b[1] 계산 (1 사이클)
...
100번 반복하면 100 사이클이 걸립니다. (Scalar Operation).
최신 CPU(Intel AVX, ARM NEON)와 GPU는 이렇게 동작합니다.
"야, a[0]~a[7]까지 8개랑, b[0]~b[7]까지 8개 한꺼번에 가져와."
그리고 "더해!"라는 명령 한 번에 8개의 덧셈을 동시에 끝냅니다. (Vector Operation).
이게 가능하려면 데이터가 메모리에 완벽하게 붙어있어야(Contiguous) 합니다.
중간에 구멍이 있거나(Linked List), 주소가 흩어져 있으면 SIMD 유닛은 작동을 멈춥니다.
NVIDIA GPU가 하는 일이 뭘까요? 화면의 픽셀(배열) 수백만 개를 동시에 계산하는 것입니다. 그래서 GPU 메모리(VRAM)는 배열 처리에 목숨을 걸었습니다. 여러분이 딥러닝을 하거나 고성능 게임을 만들고 싶다면, 객체지향(OOP)이니 뭐니 하는 추상은 잠깐 잊고, 데이터를 어떻게 배열로 쫙 펼칠지(Flatten) 고민해야 합니다. 이것이 Data-Oriented Design (데이터 지향 설계)의 핵심입니다.
최신 언어들이 Set, Map, Stream 등 화려한 자료구조를 제공하지만,
결국 하드웨어 레벨에서 실행되는 것은 배열입니다.
컴퓨터 구조의 역사는 "어떻게 하면 배열을 더 빨리 돌릴까?"를 연구해온 역사와 같습니다.
화려한 추상화 뒤에 숨겨진 기계적 공감을 잊지 마세요.
튜링상을 받은 전설적인 컴퓨터 과학자들은 여전히 비트와 배열을 사랑합니다.
세상은 복잡한 자료구조를 좋아하지만, 실제로 CPU가 가장 사랑하고 가장 빠른 자료구조는 예나 지금이나 배열입니다. 단순함이 복잡함을 이깁니다.
One reason High-Performance languages (C++, Rust) love Arrays vs Java/Python/JS is Garbage Collection (GC).
Object[]. The array just holds references (pointers). The actual objects are scattered in the Heap.
int[] hp, float[] x, float[] y.
In FP (Haskell, Lisp, React Redux), we prefer Immutability.
Arrays are Mutable by nature.
Changing arr[0] = 99 destroys history.
const newArr = [...oldArr, newItem].
Immer.js or Immutable.js which use Structural Sharing to mimic Array speed without the copying cost.Arrays allow CPUs to guess correctly.
When iterating for (int i=0; i<1000; i++), the Branch Predictor says "He will likely loop again."
This makes the pipeline efficient.
Linked Lists with while(node.next) are harder to predict if nodes are scattered.
The CPU pipeline stalls, waiting for memory.
Imagine a Luxury Apartment Complex. Each unit is identical in size. The unit numbers are sequential (101, 102, 103...). This is an Array.
It reserves a Contiguous Block of memory. "I claim memory from address 0x100 to 0x200! Hands off!" Because the land is reserved in one piece, it has unique strengths and weaknesses.
Why is Array access instant (O(1))?
Why is getting arr[10000] just as fast as arr[0]?
Because it uses Math, not searching.
The CPU doesn't scan. It calculates the memory address directly using this formula:
Target Address = Start Address + (Index * Data Size)
If arr starts at memory address 1000 and holds 4-byte integers:
arr[0]: 1000 + (0 * 4) = 1000arr[1]: 1000 + (1 * 4) = 1004arr[2]: 1000 + (2 * 4) = 1008arr[500]: 1000 + (500 * 4) = 3000The CPU teleports to address 3000. This is Random Access.
Calculations take constant time O(1), regardless of the index size.
Arrays hate intruders. They are like a tight line of soldiers. There is no empty space between them.
Imagine a row of 100 people sitting in a theater. You want to sit in seat #3. But it's occupied. And you insist on sitting there.
This is Shifting.
To insert one item at the beginning (index 0), N items must move.
Cost: O(N).
This is why Arrays are terrible for queues or lists with frequent middle insertions.
This is the #1 reason why Arrays beat Linked Lists in high-performance computing (Game Engines, AI, High Frequency Trading).
We think "CPU reads one integer from RAM".
False.
RAM is slow. CPU is fast.
When CPU asks for address 1000, the memory controller says:
"He's asking for 1000. He'll probably ask for 1004, 1008 soon. Let's send the whole chunk!"
It moves a Cache Line (usually 64 bytes) at once. This implies 16 integers are loaded into the L1 Cache instantly.
[0][1][2][3][4][5][6][7]...
arr[0].arr[0]...arr[15] into L1 Cache.arr[1]. CACHE HIT! (Instant).arr[2]. CACHE HIT! (Instant).[Node1] --> (Random Address 0x5000) --> [Node2] --> (Random Address 0x9000)
In benchmarks, iterating a large Array is 10x-50x faster than a Linked List, purely due to Cache Locality.
Modern CPUs (Intel AVX, ARM NEON) can process multiple data points in a single clock cycle. This is SIMD (Single Instruction, Multiple Data).
a[0] + b[0], then a[1] + b[1]... (8 cycles for 8 items)a[0]...a[7] and b[0]...b[7], add them all at once. (1 cycle)SIMD only works efficiently with Arrays (contiguous memory). It cannot optimize Linked Lists effectively.
This is why NumPy in Python is fast. It uses C-arrays and SIMD under the hood.
The most famous hacking vulnerability in history. Languages like C/C++ do not check boundaries by default for performance.
char name[8] (8 bytes).name in memory is the Return Address (where function goes after finishing).Defense: Always check length. Use safer languages (Rust, Java, Python) that throw IndexOutOfBoundException.
list works)"Wait, Python lists can grow infinitely. Arrays are fixed size. How?"
Python's list, Java's ArrayList, C++'s std::vector are Dynamic Arrays.
They use a strategy called Geometric Resizing.
Tip: In Java/C++, if you know you need 1,000,000 items, initialize with new ArrayList<>(1000000). This prevents the expensive resizing steps (4->8->16...->1000000).
In JavaScript, Arrays are strange.
const arr = [] is technically an Object-like structure.
BUT, the V8 engine (Chrome/Node.js) optimizes them.
[1, 2, 3]. No holes. V8 stores this as a C-array doubles. Super fast.[1, , 3]. A hole in the middle. V8 downgrades this to a Hash Map lookup. 100x slower.Tip: Never do arr[9999] = 1 on an empty array. You create 9999 holes. The engine gives up on optimization.
Always uses push() or initialize with new Array(size).fill(0).
V8 tracks element types.
PACKED_SMI_ELEMENTS: All small integers. (Fastest)PACKED_DOUBLE_ELEMENTS: Floats.PACKED_ELEMENTS: Objects/Strings.If you add a float 1.5 to an integer array, V8 secretly converts the entire array to Doubles.
If you add a String, it de-optimizes to generic pointers.
Lesson: Keep your arrays monomorphic (same type).
When you have a matrix int matrix[100][100], how is it flattened into 1D RAM?
Row by row.
[0,0], [0,1] ... [0,99], [1,0] ...
Implication:
Looping for r in rows: for c in cols is FAST (Sequential access).
Looping for c in cols: for r in rows is SLOW (Cache miss every time).
Column by column.
[0,0], [1,0] ... [99,0], [0,1] ...
If you are a Data Scientist moving code from MATLAB to Python, be careful. Your loops might need to be swapped for performance.