728x90
반응형
SMALL

고수준 프로그래밍 언어(high-level programming language)

  • 사람이 이해하기 쉬운 언어와 비슷한 형태의 프로그래밍 언어를 의미한다. 고수준 언어는 컴퓨터 하드웨어와 상대적으로 멀리 떨어져 있으며, 대부분의 사람들에게 친숙하고 이해하기 쉬운 개념들로 구성되어 있다.

고수준 프로그래밍 언어의 몇 가지 주요 특징은 다음과 같다:

  1. 추상화: 고수준 언어는 낮은 수준의 세부 사항을 숨기고, 개발자가 주로 문제 해결에 집중할 수 있도록 한다. 이러한 추상화는 개발자가 프로그램의 전반적인 로직에 더 많은 시간을 할애할 수 있게 해주며, 코드를 작성하고 디버그하는 과정을 간소화한다.
  2. 이식성: 고수준 언어로 작성된 프로그램은 일반적으로 여러 플랫폼에서 실행될 수 있다. 이는 고수준 언어의 프로그램이 특정 하드웨어나 운영 체제에 의존하지 않도록 설계되었기 때문이다.
  3. 생산성 향상: 고수준 언어는 개발자가 더 빨리 코드를 작성하고 수정할 수 있도록 설계되었다. 이는 언어의 추상화 수준이 높아져, 더 복잡한 작업을 명령문 한 줄로 수행할 수 있게 되기 때문이다.
  4. 유지 관리: 고수준 언어로 작성된 코드는 일반적으로 더 이해하기 쉽고, 수정하거나 디버그하기 편리하다. 이는 코드가 사람이 이해하기 쉬운 형식으로 작성되었기 때문이다.

고수준 프로그래밍 언어의 예로는 Python, Java, C++, JavaScript 등이 있다. 이러한 언어들은 다양한 영역에서 활용되며, 웹 개발, 데이터 분석, 인공 지능, 게임 개발 등 다양한 분야에서 사용된다.


컴파일러

  • 고수준 프로그래밍 언어로 작성된 소스 코드를 컴퓨터가 이해할 수 있는 저수준 언어(일반적으로 기계어 또는 바이트 코드)로 변환하는 소프트웨어 도구이다. 이 변환 과정을 "컴파일"이라고 한다.

컴파일러는 일반적으로 다음과 같은 두 단계로 작동한다:

  1. 전처리 및 구문 분석 단계: 이 단계에서 컴파일러는 소스 코드를 읽고, 구문을 분석하여 추상 구문 트리(Abstract Syntax Tree, AST)라는 내부 표현을 생성한다. AST는 소스 코드의 구조와 의미를 반영하며, 이 단계에서 문법 오류가 발견되면 컴파일러는 오류 메시지를 출력한다.
  2. 코드 생성 및 최적화 단계: 컴파일러는 AST를 기계어나 바이트 코드와 같은 저수준 코드로 변환한다. 이 과정에서 컴파일러는 다양한 최적화를 수행하여 생성된 코드가 가능한 효율적으로 실행될 수 있도록 한다.

컴파일러의 주요 장점 중 하나는 소스 코드를 한 번 컴파일하면, 결과로 생성된 실행 파일은 별도의 컴파일 과정 없이 바로 실행할 수 있다는 것이다. 이는 인터프리터와 대조적인 점으로, 인터프리터는 소스 코드를 라인별로 해석하고 실행하기 때문에 실행 시마다 해석 과정이 필요하다.

또한, 컴파일러는 코드 최적화를 통해 프로그램의 실행 속도를 높이는 데 중요한 역할을 한다. 하지만 반대편으로는 컴파일 과정이 인터프리터에 비해 시간이 더 걸릴 수 있다.

컴파일러의 예로는 C의 GCC, Java의 javac, Python의 PyPy 등이 있다.


고수준 언어와 어셈블리 언어의 차이점

모두 프로그래밍 언어이지만, 그들의 추상화 수준, 사용 용이성, 이식성 등 여러 측면에서 중요한 차이점이 있다.

  1. 추상화 수준: 고수준 언어는 많은 수준의 추상화를 제공한. 이는 개발자가 프로그래밍 할 때 더 복잡한 작업을 단순한 명령문으로 수행할 수 있게 해준다. 반면에 어셈블리 언어는 저수준 언어로, 하드웨어에 가까운 명령을 사용한다. 이는 더 세밀한 제어를 가능하게 하지만, 동시에 코드를 작성하고 이해하는 것을 더 복잡하게 만든다.
  2. 사용 용이성: 고수준 언어는 일반적으로 코드를 작성하고 이해하는 것이 더 쉽다. 이는 고수준 언어가 일반적으로 사람이 이해하기 쉬운 형식과 문법을 사용하기 때문이다. 반면에 어셈블리 언어는 특정 CPU 아키텍처에 대한 깊은 이해를 요구하며, 문법이 사람이 이해하기 어렵다.
  3. 이식성: 고수준 언어로 작성된 프로그램은 일반적으로 여러 플랫폼에서 실행될 수 있다. 이는 고수준 언어가 특정 하드웨어나 운영 체제에 의존하지 않도록 설계되었기 때문이다. 반면에 어셈블리 언어로 작성된 프로그램은 특정 CPU 아키텍처에 맞게 작성되므로, 다른 아키텍처에서는 실행되지 않을 수 있다.
  4. 효율성: 어셈블리 언어는 저수준에서 동작하기 때문에, 필요한 경우 최적화를 위해 사용될 수 있다. 하지만, 이는 프로그래머가 하드웨어에 대한 매우 깊은 이해를 가져야 한다는 것을 의미한다. 반면에 고수준 언어는 자동화된 메모리 관리, 오류 검사 등의 기능을 제공함으로써 프로그래머의 작업을 단순화하지만, 이러한 편의성은 때때로 성능에 부정적인 영향을 미칠 수 있다.

요약하면, 고수준 언어는 개발자의 편의성과 코드의 이식성에 중점을 두고 있으며, 어셈블리 언어는 효율성과 세밀한 제어에 중점을 두고 있다. 그러나 최신 고수준 언어의 컴파일러는 상당히 효율적인 기계 코드를 생성할 수 있으므로, 대부분의 경우에는 어셈블리 언어를 직접 사용할 필요는 없다.


고수준 프로그래밍 언어는 어셈블리 언어에 비해 여러 가지 중요한 이점을 가지고 있다:

  1. 사용 편의성: 고수준 언어의 문법은 일반적으로 사람이 이해하기 쉽다. 이는 코드를 작성하고 이해하는 것을 더욱 쉽게 해주며, 프로그래밍 초보자가 학습하기에도 더 적합하다.
  2. 시간 효율성: 고수준 언어는 코드를 빠르게 작성하고 수정하는 데 유용하다. 복잡한 작업을 몇 줄의 코드로 수행할 수 있기 때문에 개발 시간이 크게 단축된다.
  3. 이식성: 고수준 언어로 작성된 프로그램은 여러 플랫폼에서 실행될 수 있다. 즉, 한 번 작성하면 다양한 컴퓨터 아키텍처에서 실행할 수 있다. 반면에, 어셈블리 언어로 작성된 코드는 특정 하드웨어에 대해 작성되므로 이식성이 낮다.
  4. 오류 발견 및 디버깅 용이성: 고수준 언어는 문제 발생 시 오류 메시지를 제공하거나 디버깅 도구를 사용하여 문제를 쉽게 찾고 수정할 수 있다. 어셈블리 언어에서는 이러한 프로세스가 훨씬 더 복잡하고 어려울 수 있다.
  5. 추상화: 고수준 언어는 메모리 관리, 가비지 컬렉션, 보안 기능 등과 같은 중요한 작업을 자동으로 처리한다. 이는 개발자가 복잡한 세부 사항에 신경 쓸 필요 없이 문제 해결에 집중할 수 있게 해준다.
  6. 풍부한 라이브러리 및 프레임워크: 고수준 언어는 웹 개발, 데이터베이스 접근, 그래픽 처리, 기계 학습 등과 같은 많은 작업을 위한 라이브러리와 프레임워크를 제공한다. 이는 개발자가 코드를 빠르게 작성하고, 기존의 테스트된 코드를 재사용할 수 있게 해준다.

이런 이점들 때문에, 대부분의 현대 소프트웨어 개발은 고수준 언어를 사용하여 진행된다. 그러나 어셈블리 언어는 특정 작업에서 최고의 성능을 필요로 할 때, 혹은 하드웨어에 매우 가까운 수준에서 작동해야 할 때 여전히 중요할 수 있다.


프로그래밍 언어는 기본적으로 컴퓨터에게 명령을 내리기 위한 도구이며, 언어마다 공통적인 요소와 독특한 특징이 있다.

프로그래밍 언어 간의 일반적인 유사점은 다음과 같다:

  1. 기본적인 구성 요소: 대부분의 프로그래밍 언어는 변수, 연산자, 조건문, 반복문, 함수 등과 같은 기본적인 구성 요소를 갖고 있다. 이러한 요소들은 데이터를 조작하고, 로직을 제어하며, 코드를 재사용 가능한 단위로 구조화하는 데 사용된다.
  2. 명령적 프로그래밍: 대부분의 언어는 명령형 프로그래밍 패러다임을 따른다. 즉, 프로그램은 일련의 명령으로 구성되며, 이 명령은 순서대로 실행된다.
  3. 타입 시스템: 대부분의 프로그래밍 언어는 타입 시스템을 가지고 있다. 이는 프로그래머가 데이터의 종류를 지정하고, 프로그램이 예상대로 동작하도록 돕는다.

프로그래밍 언어 간의 주요 차이점은 다음과 같다:

  1. 문법: 프로그래밍 언어마다 자체의 고유한 문법을 가지고 있다. 이는 언어를 배우고 사용하는 데 중요한 역할을 하며, 개발자의 선호도와 작업의 특성에 따라 선택하는 언어가 달라질 수 있다.
  2. 프로그래밍 패러다임: 언어는 명령형, 객체 지향, 함수형, 논리형 등 다양한 프로그래밍 패러다임을 지원할 수 있다. 이러한 패러다임은 언어가 문제를 해결하는 방식을 크게 영향을 미친다.
  3. 성능과 용도: 언어마다 성능과 용도에 있어 차이가 있다. 예를 들어, C++와 같은 언어는 시스템 프로그래밍이나 게임 개발에서 선호되며, Python은 데이터 과학과 웹 개발에서 널리 사용된다.
  4. 타입 체크: 일부 언어는 컴파일 시간에 타입을 검사하는 반면(예: Java, C++), 다른 언어는 런타임에 타입을 검사한다(예: Python, JavaScript).
  5. 메모리 관리: 일부 언어(예: C, C++)는 개발자가 메모리를 직접 관리해야 하는 반면, 다른 언어(예: Java, Python)는 가비지 컬렉션 등의 자동 메모리 관리 기능을 제공한다.

이와 같은 유사점과 차이점을 이해하면, 어떤 언어가 특정 작업에 가장 적합한지 결정하는 데 도움이 될 수 있다.


특정 프로세서 아키텍처에 독립적인 고수준 프로그래밍 언어의 개발이다. 고수준 언어를 쓰면 사람이 표현하는 방식에 가까운 용어로 계산 과정을 작성할 수 있다.

고수준 언어로 작성된 코드는 번역기 프로그램을 통해 대상 프로세서의 어셈블리 언어로 된 명령어로 변환한 다음, 어셈블러에 의해 비트로 변환되어 메모리에 로드되고 실행된다.

번역기 프로그램은 보통 컴파일러라고 불리는데, 그다지 통찰력이나 직관이 느껴지지 않는 역사적 용어이다.

일반적인 고수준 언어에서는 두 수 X와 Y를 더하고 결과를 Z에 저장하는 계산이 다음과 같이 표현한다.

Z=X+Y

X와 Y라는 메모리 위치에서 값ㅇ르 가져와서, 두 값을 더하고, Z라는 메모리 위치에 결과를 저장한다.

연산자 ‘=’는 같다가 아니라 대체한다 or 저장한다라는 뜻이다.

모형 컴퓨터용 컴파일러는 이 코드를 세 개의 명령어로 변환하겠지만, 또 다른 컴퓨터용 컴파일러는 두 개의 명령어로 변환할 수도 있을 것이다.


다음으로, 각 어셈블러는 각자의 어셈블리 언어 명령어를 실제 명령어의 비트 패턴으로 변환하는 일 뿐만 아니라 변수 X, Y, Z를 저장할 메모리 이치를 확보하는 일을 담당한다. 비트패턴은 각각의 컴퓨터의 대해 거의 완전히 다르게 변환된다.

실제로 컴파일러가 내부적으로 하나의 ‘프런트 엔드’와 몇 개의 ‘백엔드’로 나뉠 가능성이 있다.

프런트 엔드는 고수준 프로그래밍 언어로 작성된 프로그램을 처리해서 어떤 중간 형태로 만들고, 각 백엔드 엔드는 공통된 중간 표현을 특정 아키텍처에 맞는 어셈블리 언어로 변환한다. 이 구조는 완전히 독립적인 여러 개의 컴파일러를 이용하는 것보다 단순하다.

고수준 언어는 어셈블리 언어에 비해 큰 이점을 갖는다.

  • 사람들이 생각하는 방식에 더 가까워 배우고 사용하기 더 쉽다.
  • 고수준 언어에서는 프로그램을 효율적으로 짜기 위해 특정 프로세서의 명령어 레퍼토리를 알아야 할 필요가 없다. 따라서 더 많은 이들이 더 빨리 프로그래밍을 할 수 있게 해준다.

또한 고수준 언어로 작성된 프로그램은 특정 아키텍처에 종속되지 않는다. 그래서 같은 프로그램이 여러 아키텍처상에서 실행될 수 있다. 보통은 코드를 변경할 필요도 없으며, 위 그림처럼 다른 컴파일러로 컴파일하기만 하면된다.

컴파일 단계는 몇 가지 명백한 에러를 미리 점검하게 해준다. 철자 오류, 괄호 불일치 같은 구문 오류, 정의되지 않은 변수에 대한 연산 같은 것들이 포함됨

어셈블리 언어 프로그램에서 구문 오류 이외의 에러는 검출하기 어렵다. 어셈블러는 명령어의 실행 순서까지는 상관하지 않아서 논리적인 흐름이나 전후 관계를 파악하지 못하기 때문이다. (고수준 언어에서도 구문상 올바르다고 판단되는 프로그램이 여전히 컴파일러가 검출할 수 없는 에러로 가득할 수도 있다.)

프로그래밍 언어 간의 유사점과 차이점을 살펴볼 수 있도록 가장 중요한 고수준 프로그래밍 언어 여섯 가지로 작성된 동일 기능을 볼 수 있다.


컴파일러

  • 고수준 언어를 어셈블리 언어로 변환하는 번역기 프로그램(책 설명)

컴파일러 VS 인터프리터

  컴파일러 인터프리터
정의 고수준 언어로 작성된 프로그램 전체를 목적 프로그램으로 번역한 후, 링킹작업을 통해 컴퓨터에서 실행 가능한 실행 프로그램을 생성 고수준 언어로 작성된 프로그램을 한 줄 단위로 받아들여 번역하고, 번역과 동시에 프로그램을 한 줄 단위로 즉시 실행
장점 반복 처리가 많을 경우, 번역과 실행 과정을 거쳐야 하기 때문에 번역 시간이 오래 걸리지만, 한 번 번역한 후에는 다시 번역하지 않으므로 실행 속도가 빠르다 번역 시, 그때그때 필요한 실행 코드를 생성하므로 사용하는 메모리가 적어 별도의 기억 공간이 적게 필요. 줄 단위로 번역 실행되므로 시분할 시스템에 유용,
단점 컴파일 시 전체 프로그램 코드가 생성되므로 사용하는 메모리가 많아짐 → 별도의 많은 기억 공간 필요 반복 처리가 많은 경우, 번역 속도는 빠르지만 프로그램 실행 시 매번 번역해야 하므로 실행 속도는 느림
언어 C, C++, Java, COBOL, FORTRAN R, Rython, Ruby
실행 속도 빠름 느림
번역 속도 느림 빠름

 

728x90
반응형
LIST
728x90
반응형
SMALL

어셈블러(assembler)

  • 컴퓨터에서 사용하는 저급 프로그래밍 언어인 어셈블리 언어를 기계어로 변환하는 소프트웨어
  • 어셈블리 언어는 컴퓨터의 기계어에 대응하는 기호적인 표현을 사용하기 때문에, 각 기계어 명령어는 어셈블리 언어의 명령어와 일대일 대응이 가능하다.
  • 어셈블러의 주요 작업은 어셈블리 언어의 각 명령어를 해당하는 기계어 코드로 변환하는 것
  • 이런 변환은 기본적으로 대체 작업이지만, 레이블을 메모리 주소에 맵핑하는 등의 추가 작업도 포함될 수 있다.

어셈블리 언어는 컴퓨터 아키텍처에 매우 의존적이다. 즉, 서로 다른 컴퓨터 아키텍처에서는 동일한 기능을 가진 명령어라도 다른 어셈블리 코드를 가질 수 있다. 따라서, 특정 컴퓨터 아키텍처에 대한 깊은 이해 없이는 어셈블리 언어를 효과적으로 사용하는 것이 어렵다.

어셈블리 언어는 주로 시스템 프로그래밍에서 사용되며, 시스템의 하드웨어나 운영체제와 직접적으로 상호작용해야 하는 코드를 작성할 때 주로 사용된다. 이러한 경우, 고급 프로그래밍 언어에서는 처리하기 어려운 저수준의 작업을 수행할 수 있다.


어셈블리 언어

  • 컴퓨터 프로그래밍 언어 중에서 가장 저수준의 언어 중 하나로, 기계어 코드에 직접적으로 대응하는 사람이 읽을 수 있는 코드를 제공
  • 기본적으로 컴퓨터 아키텍처의 기계어 명령 집합에 대한 기호적인 표현
  • 어셈블리 언어는 각기 다른 CPU 아키텍처마다 고유한 명령어 집합을 가진다.
  • 즉, 어셈블리 언어로 작성된 프로그램은 그 프로그램이 작성된 특정 컴퓨터 아키텍처에서만 실행될 수 있다.

어셈블리 언어는 다음과 같은 특징을 가지고 있다:

  1. 기계 친화적: 어셈블리 언어는 기계어와 매우 밀접한 관련이 있어, 시스템의 내부 작동 방식을 제어하는 데 매우 유용하다.
  2. 성능 최적화: 프로그래머가 메모리나 프로세서 명령을 직접 제어할 수 있으므로, 성능을 최적화하는 데 필요한 세밀한 제어를 할 수 있다.
  3. 하드웨어 상호작용: 특정 하드웨어를 제어하거나 운영체제의 저수준 기능을 활용해야 하는 경우에 사용된다.

그러나 어셈블리 언어는 프로그래밍이 복잡하고, 디버깅이 어렵고, 코드를 이해하거나 유지보수하기 어렵다는 단점이 있다. 또한 어셈블리 언어로 작성된 코드는 해당 아키텍처에 종속적이므로, 다른 아키텍처에서는 재사용할 수 없다.

오늘날 대부분의 소프트웨어 개발은 고급 프로그래밍 언어를 사용하여 이루어지지만, 임베디드 시스템, 컴퓨터 바이러스, 운영체제, 게임 엔진 등 특정 영역에서는 여전히 어셈블리 언어가 사용된다.


어셈블리 언어 프로그래밍

  • 저수준의 컴퓨팅 작업에서 요구되는 것과 같은 특정 작업을 수행하기 위해 어셈블리 언어를 사용하는 프로그래밍 작업
  • 어셈블리 언어 프로그래밍은 컴퓨터의 중앙 처리 장치(CPU)가 수행할 수 있는 명령어를 직접 기술하는 방식으로 이루어진다.
  • 이 명령어들은 컴퓨터 메모리의 특정 위치를 지정하거나, 계산을 수행하거나, 입력/출력 작업을 실행하거나, 조건에 따라 프로그램의 흐름을 제어하는 등의 작업을 수행한다.

어셈블리 언어 프로그래밍은 아래와 같은 몇 가지 단계로 이루어진다:

  1. 코드 작성: 어셈블리 언어로 코드를 작성한다. 이는 프로그래머가 특정 작업을 수행하기 위해 CPU에 내릴 명령어를 명시하는 것을 포함한다.
  2. 어셈블리: 작성한 어셈블리 코드는 어셈블러를 통해 기계어로 변환된다. 기계어는 컴퓨터가 직접 이해하고 실행할 수 있는 언어이다.
  3. 링킹: 여러 어셈블리 코드 파일이 하나의 실행 가능한 프로그램으로 결합된다. 이 단계는 링커(linker)라는 도구를 통해 이루어진다.
  4. 디버깅 및 최적화: 프로그램의 작동을 확인하고, 문제가 있으면 디버깅 필요한 경우, 프로그램의 실행 속도나 메모리 사용량을 줄이기 위해 코드를 최적화한다.

어셈블리 언어 프로그래밍은 복잡하고 시간이 많이 걸리며, 종종 고급 프로그래밍 언어를 사용하는 것보다 훨씬 어렵다. 그러나 어셈블리 언어를 사용하면 하드웨어를 더욱 세밀하게 제어하고, 시스템의 성능을 극대화하는 등의 이점이 있다. 따라서 하드웨어에 대한 깊은 이해가 필요한 프로그래밍 작업, 예를 들어 임베디드 시스템, 드라이버, 운영체제 등에서 어셈블리 언어 프로그래밍이 요구되곤 한다.


용어 정리

  • 어셈블러(assembler) : 특정한 처리를 수행하는 프로그램, 다른 프로그래머 사전에 작성했던 프로그램에서 필요한 부분을 모으는 역할을 하기도 했기에 붙은 이름
  • 어셈블리 언어 : 어셈블러 프로그램 작성에 사용되는 언어
  • 어셈블리 언어 프로그래밍 : 어셈블러 프로그램을 작성하는 프로그래밍

어셈블러의 장점

  • 프로그램을 수정하는 일을 쉽게 해준다.
  • 명령을 추가/삭제 할때 변경 기록을 직접 관리하는 대신 어셈블러가 각 명령어와 데이터 값이 메모리상 어느 위치에 있을지 파악해주기 때문에.

어셈블러의 특징

  • 프로세서의 명령어와 일대일로 연결되는 해당 아키텍처에 특화된 언어이다.
    • ex) 맥과 PC 인텔의 어셈블리 언어는 다르다.
  • 명령어가 이진수로 인코딩 되는 특정한 방식과 메모리에 정보가 배치되는 방식 등을 알고 있다.
  • 즉, 특정 컴퓨터용 프로그램이 다른 컴퓨터에서 실행되도록 변환하려면 프로그래머는 두 프로세서의 세부사항을 모두 숙달하여야 한다.

어셈블러의 기원과 역사

  • 1950년대 , 프로그래밍 시 단순 반복 작업을 처리하기 위한 프로그램을 만들었는데, 이것이 어셈블러의 기원이다.
  • 최초의 전자식 컴퓨터에서의 프로그래밍은 카드나 종이에 구멍을 뚫어서 그 수를 기계가 판독하게 만들어야 했다. 즉, 수동으로 명렁어와 데이터를 변경 또는 추가를 해야했고, 프로그램을 바꾸는 것이 매우 어려웠다.
    • 여기서 패치의 기원이나온다. 원래 뜻은 옷에 뚫린 구멍을 덮을 때 쓰는 천 쪼가리 이지만, 구천공카드에 뚫린 구멍을 매꾸는 고침(수정)이 패치라는 언어로 굳어졌다.
728x90
반응형
LIST
728x90
반응형
SMALL

DFS(깊이 우선 탐색, Depth-First Search)는 그래프나 트리를 탐색하는 알고리즘 중 하나이다. 이 방법은 주어진 그래프의 모든 정점(Vertex)을 방문하는 경로를 찾을 때 사용한다.

DFS는 이름에서 알 수 있듯이, 깊이 방향으로 우선 탐색하는 것을 기본 원리로 한다. 즉, 시작 노드에서 가능한 한 깊숙히 들어가면서 노드를 탐색하고, 더 이상 탐색할 자식 노드가 없을 경우 이전 노드로 돌아와서 그 다음 노드를 탐색하는 방식이다.

DFS의 동작 과정은 다음과 같다:

  1. 시작 노드를 선택하여 방문하고 방문한 노드를 스택에 push한다.
  2. 현재 방문한 노드에 인접한 노드 중에서 아직 방문하지 않은 노드가 있다면 그 노드를 방문하고 방문한 노드를 스택에 push한다. 만약 방문하지 않은 인접 노드가 없다면 스택에서 최상단 노드를 pop한다.
  3. 더 이상 2번의 과정을 반복할 수 없을 때까지 계속 반복한다.

DFS는 스택 또는 재귀 호출을 이용하여 구현할 수 있으며, 그래프의 모든 노드를 방문하고 싶을 때, 경로 탐색을 해야할 때 주로 사용된다.

import java.util.*;

class Graph {
    private int numOfNodes;  // 노드의 수를 저장
    private LinkedList<Integer> adjListArray[];  // 인접 리스트 배열

    // 생성자
    Graph(int numOfNodes) {
        this.numOfNodes = numOfNodes;  // 노드의 수 설정
        // 배열의 크기를 노드의 수로 설정
        adjListArray = new LinkedList[numOfNodes];

        // 각 노드마다 인접 노드를 저장할 리스트 생성
        for (int i = 0; i < numOfNodes; i++) {
            adjListArray[i] = new LinkedList<>();
        }
    }

    // 무방향 그래프에 간선 추가
    void addEdge(int src, int dest) {
        // src에서 dest로의 간선 추가
        adjListArray[src].add(dest);
        // dest에서 src로의 간선 추가
        adjListArray[dest].add(src);
    }

    // 노드 u에서 시작하는 그래프의 DFS를 실행하는 함수
    void DFSUtil(int node, boolean[] visited) {
        // 현재 노드를 방문했다고 표시하고 노드 출력
        visited[node] = true;
        System.out.print(node + " ");

        // 이 정점과 인접한 모든 정점에 대해 재귀적으로 탐색
        Iterator<Integer> iterator = adjListArray[node].listIterator();
        while (iterator.hasNext()) {
            int n = iterator.next();
            if (!visited[n])
                DFSUtil(n, visited);
        }
    }

    // DFS 순회를 수행하는 함수. 재귀적인 DFSUtil()을 사용함.
    void DFS(int startNode) {
        // 모든 정점을 방문하지 않았다고 표시
        boolean[] visited = new boolean[numOfNodes];

        // 재귀적 도우미 함수를 호출하여 모든 정점에서 DFS 순회를 출력
        DFSUtil(startNode, visited);
    }
}

public class Main {
    public static void main(String args[]) {
        Graph graph = new Graph(5);  // 5개의 노드를 가진 그래프 생성
        graph.addEdge(0, 1);  // 노드 0과 1 사이에 간선 추가
        graph.addEdge(0, 4);  // 노드 0과 4 사이에 간선 추가
        graph.addEdge(1, 2);  // 노드 1과 2 사이에 간선 추가
        graph.addEdge(1, 3);  // 노드 1과 3 사이에 간선 추가
        graph.addEdge(1, 4);  // 노드 1과 4 사이에 간선 추가
        graph.addEdge(2, 3);  // 노드 2와 3 사이에 간선 추가
        graph.addEdge(3, 4);  // 노드 3과 4 사이에 간선 추가

        System.out.println("DFS traversal of the graph starting from node 0: ");
        graph.DFS(0);  // 노드 0에서 시작하는 그래프의 DFS 순회 시작
    }
}

DFS는 그래프의 모든 노드를 방문하는 알고리즘 중 하나로, 현재 정점에서 가능한 한 깊숙이 들어가서 노드를 방문한 후, 더 이상 방문할 노드가 없을 때 이전 노드로 돌아가 다음 노드를 방문하는 방식을 취한다. 이러한 방식은 스택의 LIFO(Last In First Out) 원칙을 따르는데, 이는 재귀 함수의 호출 스택으로 쉽게 구현할 수 있다.

위 코드에서는 Graph 클래스 내에 DFSUtil 메소드를 통해 실제 DFS 알고리즘이 구현되어 있다. DFSUtil 메소드는 인접 노드를 찾아서 아직 방문하지 않은 노드를 재귀적으로 탐색한다. DFS 메소드에서는 방문 여부를 저장하는 boolean 배열 **visited**를 초기화하고 시작 노드를 지정하여 DFS를 시작한다.

이 예제에서 그래프는 다음과 같이 구성된다.

  0
 / \\
1---4
| \\ |
|  \\|
2---3

그래프에 대해 DFS 함수를 호출하면 노드 **0**에서 시작하는 DFS 탐색 결과를 콘솔에 출력하게 됩니다. 출력된 결과는 DFS 탐색 순서에 따른 노드 번호이다.


예를 들어, 아래와 같은 그래프가 있다고 가정하면

  A
 / \\
B---C
| \\ |
|  \\|
D---E

이 그래프에 DFS를 적용한다면 다음과 같은 순서로 노드를 방문하게 된다.

  1. 먼저 시작 노드인 **A**를 방문한다. (방문 노드: A)
  2. **A**에 인접한 노드 중 하나인 B를 방문한다. (A -> B)
  3. 다음으로 **B**에 인접한 노드 중 하나인 C를 방문한다. (A -> B -> C)
  4. **C**에 인접한 노드 중 **A**와 **B**는 이미 방문한 상태이므로 E를 방문한다.(A -> B -> C -> E)
  5. **E**에 인접한 노드 중 **B**와 **C**는 이미 방문했으므로 D를 방문한다. (A -> B -> C -> E -> D)
  6. 모든 노드를 방문했으므로 탐색을 종료한다.

위와 같이 깊이를 우선적으로 탐색하게 되면, 처음 시작 노드에서 가장 멀리 있는 노드를 먼저 방문하게 된다. DFS는 이와 같은 특성 때문에 미로 찾기, 경로 탐색 등에서 많이 활용된다.

 


왜... \을 쓰면 \\로 기록될까요....ㅠㅠㅠ

728x90
반응형
LIST

'CS > 알고리즘' 카테고리의 다른 글

동적 계산법  (0) 2023.06.07
큐(Queue)  (0) 2023.06.07
스택(Stack)  (0) 2023.06.07
이진 검색 트리(Binary Search Tree, BST)  (0) 2023.05.30
해싱(Hashing)  (0) 2023.05.30

+ Recent posts