트리(Tree)는 노드(Node)들이 계층적으로 연결된 비선형 자료구조다. 하나의 루트 노드에서 시작하여 자식 노드들이 가지처럼 뻗어나가는 구조로, 사이클이 없는 연결된 그래프의 특수한 형태다.
A (루트)
/ \
B C
/ \ \
D E F
/ / \
G H I
연산 | 평균 | 최악 | 설명 |
---|---|---|---|
탐색 | O(n) | O(n) | 모든 노드를 확인해야 할 수 있음 |
삽입 | O(1) | O(1) | 부모 노드에 직접 추가 |
삭제 | O(1) | O(1) | 자식 관계만 끊으면 됨 |
순회 | O(n) | O(n) | 모든 노드 방문 |
O(n) - 모든 노드를 저장
class TreeNode<T> {
data: T;
children: TreeNode<T>[];
parent: TreeNode<T> | null;
constructor(data: T) {
this.data = data;
this.children = [];
this.parent = null;
}
addChild(child: TreeNode<T>): void {
child.parent = this;
this.children.push(child);
}
removeChild(child: TreeNode<T>): boolean {
const index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
child.parent = null;
return true;
}
return false;
}
isLeaf(): boolean {
return this.children.length === 0;
}
getDepth(): number {
let depth = 0;
let current = this.parent;
while (current) {
depth++;
current = current.parent;
}
return depth;
}
getHeight(): number {
if (this.isLeaf()) {
return 0;
}
return 1 + Math.max(...this.children.map(child => child.getHeight()));
}
}
class Tree<T> {
root: TreeNode<T> | null;
constructor() {
this.root = null;
}
setRoot(data: T): TreeNode<T> {
this.root = new TreeNode(data);
return this.root;
}
// 전위순회 (Pre-order)
preOrderTraversal(node: TreeNode<T> = this.root!, visit: (data: T) => void): void {
if (!node) return;
visit(node.data);
for (const child of node.children) {
this.preOrderTraversal(child, visit);
}
}
// 후위순회 (Post-order)
postOrderTraversal(node: TreeNode<T> = this.root!, visit: (data: T) => void): void {
if (!node) return;
for (const child of node.children) {
this.postOrderTraversal(child, visit);
}
visit(node.data);
}
// 레벨순회 (Level-order / BFS)
levelOrderTraversal(visit: (data: T) => void): void {
if (!this.root) return;
const queue: TreeNode<T>[] = [this.root];
while (queue.length > 0) {
const node = queue.shift()!;
visit(node.data);
for (const child of node.children) {
queue.push(child);
}
}
}
// DFS로 노드 찾기
findNode(data: T, node: TreeNode<T> = this.root!): TreeNode<T> | null {
if (!node) return null;
if (node.data === data) return node;
for (const child of node.children) {
const found = this.findNode(data, child);
if (found) return found;
}
return null;
}
// 트리 높이
getHeight(): number {
return this.root ? this.root.getHeight() : -1;
}
// 노드 개수
getSize(node: TreeNode<T> = this.root!): number {
if (!node) return 0;
let size = 1;
for (const child of node.children) {
size += this.getSize(child);
}
return size;
}
// 배열로 변환 (레벨 순서)
toArray(): T[] {
const result: T[] = [];
this.levelOrderTraversal(data => result.push(data));
return result;
}
}
interface FileNode {
name: string;
type: 'file' | 'folder';
children?: FileNode[];
}
const FileExplorer: React.FC<{ rootFolder: FileNode }> = ({ rootFolder }) => {
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
const toggleFolder = (folderName: string) => {
setExpandedFolders(prev => {
const newSet = new Set(prev);
if (newSet.has(folderName)) {
newSet.delete(folderName);
} else {
newSet.add(folderName);
}
return newSet;
});
};
const renderNode = (node: FileNode, depth: number = 0): JSX.Element => {
const isExpanded = expandedFolders.has(node.name);
const indent = depth * 20;
return (
<div key={node.name}>
<div
style={{ paddingLeft: `${indent}px` }}
className={`file-item ${node.type}`}
onClick={() => node.type === 'folder' && toggleFolder(node.name)}
>
{node.type === 'folder' && (
<span className="folder-icon">
{isExpanded ? '📂' : '📁'}
</span>
)}
{node.type === 'file' && <span className="file-icon">📄</span>}
<span>{node.name}</span>
</div>
{node.type === 'folder' && isExpanded && node.children && (
<div className="folder-contents">
{node.children.map(child => renderNode(child, depth + 1))}
</div>
)}
</div>
);
};
return (
<nav className="file-explorer" role="tree">
{renderNode(rootFolder)}
</nav>
);
};
interface MenuItem {
id: string;
label: string;
url?: string;
children?: MenuItem[];
}
const NavigationMenu: React.FC<{ menuItems: MenuItem[] }> = ({ menuItems }) => {
const [activeMenu, setActiveMenu] = useState<string | null>(null);
const renderMenuItem = (item: MenuItem, level: number = 0): JSX.Element => {
const hasChildren = item.children && item.children.length > 0;
const isActive = activeMenu === item.id;
return (
<li key={item.id} className={`menu-item level-${level}`}>
<button
className={`menu-button ${isActive ? 'active' : ''}`}
onClick={() => setActiveMenu(isActive ? null : item.id)}
aria-expanded={hasChildren ? isActive : undefined}
aria-haspopup={hasChildren ? 'menu' : undefined}
>
{item.label}
{hasChildren && (
<span className="menu-arrow" aria-hidden="true">
{isActive ? '▼' : '▶'}
</span>
)}
</button>
{hasChildren && isActive && (
<ul className="submenu" role="menu">
{item.children!.map(child => renderMenuItem(child, level + 1))}
</ul>
)}
</li>
);
};
return (
<nav role="menubar">
<ul className="main-menu">
{menuItems.map(item => renderMenuItem(item))}
</ul>
</nav>
);
};
// 트리 생성
const orgChart = new Tree<string>();
// 루트 설정
const ceo = orgChart.setRoot("CEO");
// 부서장들 추가
const cto = new TreeNode("CTO");
const cfo = new TreeNode("CFO");
const cmo = new TreeNode("CMO");
ceo.addChild(cto);
ceo.addChild(cfo);
ceo.addChild(cmo);
// 개발팀 추가
const frontendLead = new TreeNode("Frontend Lead");
const backendLead = new TreeNode("Backend Lead");
cto.addChild(frontendLead);
cto.addChild(backendLead);
// 순회해서 출력
console.log("전체 조직:");
orgChart.preOrderTraversal((name) => console.log(name));
// 특정 직책 찾기
const found = orgChart.findNode("Frontend Lead");
console.log(`깊이: ${found?.getDepth()}`); // 2
다음에는 이진 트리에 대해 알아보자. 이진 트리는 각 노드가 최대 2개의 자식만 가질 수 있는 특별한 형태의 트리다.