Merge sort

병합 정렬

Split an array in halves, then merge sorted pieces in order.

···
css
body{display:block} .stage{position:absolute;inset:0;display:flex;flex-direction:column;padding:clamp(9px,3vmin,20px);gap:5px}
.top{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:clamp(11px,2.7vmin,14px);font-weight:700;white-space:nowrap}
.top span:last-child{color:var(--muted);font-weight:500}canvas{width:100%;height:0;flex:1 1 0;min-height:0}
.controls{display:none;align-items:center;gap:9px;font-size:12px;color:var(--muted)}
button{border:1px solid var(--line);background:var(--surface);color:var(--fg);border-radius:7px;padding:5px 10px;cursor:pointer}
input{width:100px;accent-color:var(--accent)}
@media(min-width:600px){.controls{display:flex}}
js
const ALGORITHM_KIND = "merge-sort";

const kind = ALGORITHM_KIND;
const root = document.createElement('div'); root.className = 'stage';
root.innerHTML = '<div class="top"><span id="state"></span><span id="count"></span></div><canvas></canvas><div class="controls"><button id="reset">다시 시작</button><button id="step">한 단계</button><label>속도 <input id="speed" type="range" min="1" max="5" value="3"></label></div>';
document.body.append(root);
const canvas = root.querySelector('canvas'), ctx = canvas.getContext('2d');
const title = root.querySelector('#state'), count = root.querySelector('#count');
const style = getComputedStyle(document.documentElement);
const colors = Object.fromEntries(['bg','fg','muted','line','accent','accent-2','accent-3','surface'].map(k => [k, style.getPropertyValue('--' + k).trim()]));
const palette = {base:colors.accent, active:colors['accent-2'], done:colors['accent-3'], text:colors.fg, faint:colors.muted, line:colors.line, surface:colors.surface};
let generation = 0, states = [], index = 0, elapsed = 0;
function rng(seed){return function(){seed |= 0;seed = seed + 0x6D2B79F5 | 0;let t = Math.imul(seed ^ seed >>> 15, 1 | seed);t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;return ((t ^ t >>> 14) >>> 0) / 4294967296}}
function shuffled(){const a=[3,8,2,7,4,9,1,6,5];const random=rng(42+generation*23);for(let i=a.length-1;i>0;i--){const j=Math.floor(random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}
function push(values, active=[], done=[], note='', extra={}){states.push({values:[...values],active:[...active],done:[...done],note,extra})}
function makeStates(){
  states=[];
  const a=shuffled(), n=a.length;
  push(a,[],[],'시작');
  if(kind==='big-o') {states=[];for(let size=1;size<=12;size++)push([],[],[],'입력 크기 n = '+size,{size});return}
  if(kind==='bubble-sort'){for(let end=n-1;end>0;end--){for(let j=0;j<end;j++){push(a,[j,j+1],Array.from({length:n-end-1},(_,k)=>n-1-k),'이웃한 두 값 비교');if(a[j]>a[j+1]){[a[j],a[j+1]]=[a[j+1],a[j]];push(a,[j,j+1],[],'순서가 반대라 교환')}}push(a,[],Array.from({length:n-end},(_,k)=>n-1-k),'가장 큰 값 확정')}push(a,[],a.map((_,i)=>i),'정렬 완료')}
  if(kind==='selection-sort'){for(let i=0;i<n;i++){let min=i;for(let j=i+1;j<n;j++){push(a,[min,j],Array.from({length:i},(_,k)=>k),'최솟값 탐색');if(a[j]<a[min])min=j}[a[i],a[min]]=[a[min],a[i]];push(a,[i,min],Array.from({length:i+1},(_,k)=>k),'최솟값을 앞에 배치')}}
  if(kind==='insertion-sort'){for(let i=1;i<n;i++){let j=i;while(j>0&&a[j-1]>a[j]){push(a,[j-1,j],[],'앞의 값과 비교');[a[j-1],a[j]]=[a[j],a[j-1]];push(a,[j-1,j],Array.from({length:i+1},(_,k)=>k),'왼쪽으로 삽입');j--}push(a,[],Array.from({length:i+1},(_,k)=>k),'왼쪽 부분 정렬됨')}}
  if(kind==='merge-sort'){function merge(lo,hi){if(hi-lo<=1)return;const mid=(lo+hi)>>1;push(a,Array.from({length:hi-lo},(_,i)=>lo+i),[],'구간 분할: '+lo+'–'+(hi-1));merge(lo,mid);merge(mid,hi);const sorted=[...a.slice(lo,mid)], right=[...a.slice(mid,hi)];let l=0,r=0;for(let k=lo;k<hi;k++){a[k]=r>=right.length||(l<sorted.length&&sorted[l]<=right[r])?sorted[l++]:right[r++];push(a,[k],Array.from({length:k-lo+1},(_,i)=>lo+i),'작은 값부터 병합')}}merge(0,n);push(a,[],a.map((_,i)=>i),'정렬 완료')}
  if(kind==='quick-sort'){function quick(lo,hi){if(lo>=hi)return;const pivot=a[hi];let i=lo;push(a,[hi],[],'피벗 '+pivot+' 선택');for(let j=lo;j<hi;j++){push(a,[j,hi],[],'피벗과 비교');if(a[j]<pivot){[a[i],a[j]]=[a[j],a[i]];push(a,[i,j],[],'작은 값을 왼쪽으로');i++}}[a[i],a[hi]]=[a[hi],a[i]];push(a,[i],[i],'피벗 자리 확정');quick(lo,i-1);quick(i+1,hi)}quick(0,n-1);push(a,[],a.map((_,i)=>i),'정렬 완료')}
  if(kind==='heap-sort'){function sift(size,i){while(2*i+1<size){let child=2*i+1;if(child+1<size&&a[child+1]>a[child])child++;push(a,[i,child],[],'부모와 큰 자식 비교');if(a[i]>=a[child])break;[a[i],a[child]]=[a[child],a[i]];push(a,[i,child],[],'큰 값을 위로');i=child}}for(let i=(n>>1)-1;i>=0;i--)sift(n,i);for(let end=n-1;end>0;end--){[a[0],a[end]]=[a[end],a[0]];push(a,[0,end],Array.from({length:n-end},(_,k)=>end+k),'최댓값을 끝으로');sift(end,0)}push(a,[],a.map((_,i)=>i),'정렬 완료')}
  if(kind==='counting-sort'){const freq=Array(10).fill(0);for(let i=0;i<n;i++){freq[a[i]]++;push(freq,[a[i]],[],'값 '+a[i]+'의 개수 세기',{histogram:true})}let k=0;for(let value=1;value<=9;value++)while(freq[value]-- > 0){a[k++]=value;push(a,[k-1],Array.from({length:k},(_,i)=>i),'개수대로 출력')}push(a,[],a.map((_,i)=>i),'정렬 완료')}
  if(kind==='binary-search'){states=[];const b=[1,2,3,4,5,6,7,8,9];let lo=0,hi=8,target=7;push(b,[],[],'정렬된 배열에서 7 찾기');while(lo<=hi){let mid=(lo+hi)>>1;push(b,[mid],[],'가운데 '+b[mid]+'와 비교',{range:[lo,hi]});if(b[mid]===target){push(b,[mid],[mid],'7 찾음');break}if(b[mid]<target)lo=mid+1;else hi=mid-1;push(b,[],[],'절반 제외',{range:[lo,hi]})}}
  if(kind==='two-pointers'){states=[];const b=[1,2,3,4,5,6,7,8,9];let l=0,r=8;push(b,[l,r],[],'합계 10 찾기');while(l<r){push(b,[l,r],[],'합 '+(b[l]+b[r])+' 비교');if(b[l]+b[r]===10){push(b,[l,r],[l,r],'합계 10인 쌍');l++;r--}else if(b[l]+b[r]<10)l++;else r--}}
  if(kind==='sliding-window'){states=[];const b=[2,1,4,3,5,2,6,1,4];let sum=b.slice(0,3).reduce((x,y)=>x+y,0);push(b,[0,1,2],[],'크기 3, 합 '+sum);for(let start=1;start<=b.length-3;start++){sum+=b[start+2]-b[start-1];push(b,[start,start+1,start+2],[],'왼쪽 제거 · 오른쪽 추가 → 합 '+sum)}}
  if(kind==='stack'||kind==='queue'){states=[];const b=[];const acts=kind==='stack'?[['push',3],['push',8],['push',2],['pop'],['push',7],['pop'],['pop']]:[['enqueue',3],['enqueue',8],['enqueue',2],['dequeue'],['enqueue',7],['dequeue'],['dequeue']];push(b,[],[],'빈 '+(kind==='stack'?'스택':'큐'));for(const [op,value] of acts){let changed;if(op==='push'||op==='enqueue'){b.push(value);changed=b.length-1}else{changed=kind==='stack'?b.length-1:0;b.splice(changed,1)}push(b,[Math.max(0,Math.min(changed,b.length-1))],[],op+(value?' '+value:''),{operation:op})}}
  if(kind==='linked-list'){states=[];const nodes=[3,8,2,7].map(value=>({value,next:null}));for(let i=0;i<nodes.length-1;i++)nodes[i].next=nodes[i+1];const head=nodes[0];function snapshot(){const result=[];for(let node=head;node;node=node.next)result.push(node.value);return result}push(snapshot(),[],[],'노드와 다음 포인터');let node=head,i=0;while(node){push(snapshot(),[i++],[],'처음부터 '+node.value+' 방문');node=node.next}nodes[1].next={value:5,next:nodes[1].next};push(snapshot(),[2],[],'포인터 연결을 바꿔 5 삽입');node=head;i=0;while(node){push(snapshot(),[i++],[],'다시 순회: '+node.value);node=node.next}}
  if(kind==='hash-table'){states=[];const names=['A','F','B','G','C','H'];const buckets=Array.from({length:5},()=>[]);push([],[],[],'5개 버킷',{buckets:buckets.map(x=>[...x])});for(const name of names){const bucket=(name.charCodeAt(0)-65)%5;buckets[bucket].push(name);push([], [bucket],[],'키 '+name+' → 버킷 '+bucket,{buckets:buckets.map(x=>[...x])})}}
  if(kind==='binary-search-tree'){states=[];const order=[5,3,8,1,4,7,9],tree=[];for(const value of order){let i=0;while(tree[i]!==undefined)i=value<tree[i]?2*i+1:2*i+2;tree[i]=value;push(tree,[i],[],'값 '+value+' 삽입',{tree:true})}let i=0;while(tree[i]!==4){push(tree,[i],[],'4와 '+tree[i]+' 비교',{tree:true});i=4<tree[i]?2*i+1:2*i+2}push(tree,[i],[i],'값 4 찾음',{tree:true})}
  if(kind==='heap-priority-queue'){states=[];const h=[];function insert(x){h.push(x);let i=h.length-1;push(h,[i],[],'우선순위 '+x+' 추가');while(i>0){let p=(i-1)>>1;if(h[p]>=h[i])break;[h[i],h[p]]=[h[p],h[i]];i=p;push(h,[i],[],'큰 값을 부모로')}}for(const x of [3,8,2,7,5])insert(x);for(let t=0;t<2;t++){const max=h[0];h[0]=h.pop();push(h,[0],[],'최댓값 '+max+' 꺼냄');let i=0;while(2*i+1<h.length){let c=2*i+1;if(c+1<h.length&&h[c+1]>h[c])c++;if(h[i]>=h[c])break;[h[i],h[c]]=[h[c],h[i]];i=c;push(h,[i],[],'힙 복구')}}}
  if(kind==='recursion'){states=[];for(let d=1;d<=5;d++)push(Array.from({length:d},(_,i)=>5-i),[d-1],[],'factorial('+ (6-d) +') 호출',{call:true});let result=1;for(let d=5;d>=1;d--){result*=6-d;push(Array.from({length:d-1},(_,i)=>5-i),[],[],'반환값 '+result,{call:true})}}
  if(kind==='divide-and-conquer'){states=[];const b=[8,3,7,2,6,1,5,4];function visit(lo,hi){push(b,Array.from({length:hi-lo},(_,i)=>lo+i),[],'범위 '+lo+'–'+(hi-1)+' 분할',{range:[lo,hi-1]});if(hi-lo>1){const mid=(lo+hi)>>1;visit(lo,mid);visit(mid,hi);const left=b.slice(lo,mid),right=b.slice(mid,hi);let l=0,r=0;for(let k=lo;k<hi;k++)b[k]=r>=right.length||(l<left.length&&left[l]<=right[r])?left[l++]:right[r++];push(b,Array.from({length:hi-lo},(_,i)=>lo+i),[],'작은 결과를 병합')}}visit(0,b.length);push(b,[],b.map((_,i)=>i),'전체 결과')}
  if(kind==='prefix-sum'){states=[];const b=[2,1,4,3,5,2,6,1,4], prefix=[0];push(b,[],[],'원본 배열',{prefix:[0]});for(let i=0;i<b.length;i++){prefix.push(prefix[i]+b[i]);push(b,[i],Array.from({length:i+1},(_,j)=>j),'누적합 P['+(i+1)+'] = '+prefix[i+1],{prefix:[...prefix]})}push(b,[2,3,4,5],[],'구간 2–5 합 = P[6] − P[2] = '+(prefix[6]-prefix[2]),{prefix:[...prefix]})}
}
function resize(){const dpr=Math.min(devicePixelRatio||1,2);const w=canvas.clientWidth,h=canvas.clientHeight;canvas.width=Math.max(1,Math.round(w*dpr));canvas.height=Math.max(1,Math.round(h*dpr));ctx.setTransform(dpr,0,0,dpr,0,0);draw()}
function text(value,x,y,size=12,color=palette.text,align='center'){ctx.fillStyle=color;ctx.font='600 '+size+'px system-ui,sans-serif';ctx.textAlign=align;ctx.fillText(String(value),x,y)}
function round(x,y,w,h,r=7){ctx.beginPath();ctx.roundRect(x,y,w,h,r)}
function draw(){if(!states.length)return;const s=states[index], w=canvas.clientWidth,h=canvas.clientHeight;ctx.clearRect(0,0,canvas.width,canvas.height);title.textContent=s.note;count.textContent=(index+1)+' / '+states.length;const values=s.values;
 if(kind==='big-o'){const pad=26,gw=w-pad*2,gh=h-48;ctx.strokeStyle=palette.line;ctx.beginPath();ctx.moveTo(pad,12);ctx.lineTo(pad,h-27);ctx.lineTo(w-pad,h-27);ctx.stroke();const curves=[['O(1)',x=>1,palette.done],['O(log n)',x=>Math.log2(x+1)*5,palette.base],['O(n)',x=>x*3,palette.active],['O(n²)',x=>x*x*.65,palette.faint]];for(const [label,fn,color] of curves){ctx.strokeStyle=color;ctx.lineWidth=2;ctx.beginPath();for(let x=1;x<=s.extra.size;x++){const px=pad+(x/12)*gw,py=h-27-Math.min(gh,fn(x)/100*gh);if(x===1)ctx.moveTo(px,py);else ctx.lineTo(px,py)}ctx.stroke()}curves.forEach(([label,_,color],i)=>text(label,pad+38+i*(gw/4),h-6,10,color));return}
 if(kind==='hash-table'){const buckets=s.extra.buckets;const gap=w/5;for(let i=0;i<5;i++){const x=i*gap+gap*.08;round(x,h*.18,gap*.84,h*.64);ctx.fillStyle=s.active.includes(i)?palette.active:palette.surface;ctx.fill();ctx.strokeStyle=palette.line;ctx.stroke();text(i,x+gap*.42,h*.14,12,palette.faint);buckets[i].forEach((v,j)=>text(v,x+gap*.42,h*.3+j*20,12,s.active.includes(i)?colors.bg:palette.text))}return}
 if(kind==='binary-search-tree'||kind==='heap-priority-queue'){const nodes=values;const positions=[];for(let i=0;i<nodes.length;i++){const depth=Math.floor(Math.log2(i+1)),first=2**depth-1,slot=i-first;positions.push([(slot+1)*w/(2**depth+1),(depth+1)*h/4])}ctx.strokeStyle=palette.line;for(let i=1;i<nodes.length;i++){const p=(i-1)>>1;ctx.beginPath();ctx.moveTo(...positions[p]);ctx.lineTo(...positions[i]);ctx.stroke()}nodes.forEach((v,i)=>{const [x,y]=positions[i];ctx.beginPath();ctx.arc(x,y,Math.min(17,w/22),0,Math.PI*2);ctx.fillStyle=s.active.includes(i)?palette.active:palette.base;ctx.fill();text(v,x,y+4,12,colors.bg)});return}
 if(kind==='stack'||kind==='queue'||kind==='linked-list'||kind==='recursion'){const gap=w/Math.max(6,values.length+1);values.forEach((v,i)=>{const x=(i+1)*gap-gap*.34,y=h*.3;round(x,y,gap*.68,h*.33);ctx.fillStyle=s.active.includes(i)?palette.active:palette.base;ctx.fill();text(v,x+gap*.34,y+h*.19,14,colors.bg);if(kind==='linked-list'&&i<values.length-1)text('→',x+gap*.84,y+h*.2,16,palette.faint)});if(kind==='stack')text('위쪽에서 넣고 꺼냄',w/2,h*.83,11,palette.faint);if(kind==='queue')text('앞에서 꺼내고 뒤에 넣음',w/2,h*.83,11,palette.faint);if(kind==='recursion')text('호출 스택의 깊이',w/2,h*.83,11,palette.faint);return}
 const n=values.length,gap=w/(n+1),barWidth=Math.min(gap*.74,44),max=Math.max(9,...values);for(let i=0;i<n;i++){const x=(i+1)*gap-barWidth/2,barH=Math.max(7,values[i]/max*(h-42));const active=s.active.includes(i),done=s.done.includes(i);const outOfRange=s.extra.range&&(i<s.extra.range[0]||i>s.extra.range[1]);round(x,h-23-barH,barWidth,barH,Math.min(6,barWidth/4));ctx.fillStyle=outOfRange?palette.line:active?palette.active:done?palette.done:palette.base;ctx.fill();text(values[i],x+barWidth/2,h-7,Math.min(12,Math.max(9,gap*.3)),palette.text)}
 if(s.extra.prefix){const p=s.extra.prefix;text('P: '+p.join('  '),w/2,14,Math.min(12,w/30),palette.faint)}
}
function advance(){index=(index+1)%states.length;draw()}
function startIndex(){return kind==='big-o'?5:kind==='heap-priority-queue'?2:0}
function reset(){generation++;makeStates();index=startIndex();draw()}
root.querySelector('#reset').addEventListener('click',reset);root.querySelector('#step').addEventListener('click',advance);
addEventListener('resize',resize);makeStates();index=startIndex();resize();const speed=root.querySelector('#speed');setInterval(()=>{if(document.hidden)return;elapsed+=100;const baseDelay=kind==='divide-and-conquer'?1500:1050;if(elapsed>=baseDelay-Number(speed.value)*160){elapsed=0;advance()}},100);

Picture combining two sorted card piles by taking whichever top card is smaller.

Split the array into halves down to single values, then compare the fronts of sorted halves and merge them into temporary storage. Time is O(n log n) even in the worst case; a typical array implementation needs O(n) extra space.

Choose it when you need a predictable time bound and stable ordering of equal values. The temporary storage costs memory; the demo shows each split and the cells filled during merging.

When to use

Use when stability and a worst-case O(n log n) bound matter.

Open as page ↗