©copyright 2005
자료구조 및 실습(INA240)
교과목 소개
한국기술교육대학교 인터넷미디어공학부 김상진
강의목표 이 과목에서 문제 해결(problem solving)이란 문제에 대한 해결 정의로부터 그것을 해결하는 프로그램을 개발하는 모든 과정을 말한다. 문제를 해결하는 프로그램은 보통 두 가지 구성요소를 포함한다. 알고리즘(algorithm): 유한 단계 내에 문제를 해결하는 방법을 알고리즘 단계별로 기술한 것을 말한다. 자료구조(data structure): 데이터를 컴퓨터에 저장하는 방법 또는 자료구조 데이터를 컴퓨터로 표현하는 방법을 말한다. 이 과목에서는 여러 문제를 해결할 때 사용할 수 있는 다양한 자료구조(리스트, 스택, 큐, 트리, 그래프 등)에 대해 학습한다. Æ 이를 통해 컴퓨터를 이용한 문제해결 능력과 프로그래밍 능력을 함양한다.
2/7
강의정보 – 교수/교재 교수 정보
연구실: 제1공학관 F207호 전화번호: 교내 490 (041-560-1490) 전자우편: sangjin@kut.ac.kr 강의홈페이지: http://infosec.kut.ac.kr/sangjin/class/ds0502/ 교재
강의홈페이지에 pdf 형태의 교재가 준비되어 있음. 부교재 이석호, 자료구조와 자바, 정익사, 2004 N. Dale, D.T. Joyce, and C. Weems, Object-oriented Data Structures using Java, Jones and Bartlett, 2001. 3/7
강의정보 – 강의 평가 방법 강의 평가 방법 출석: 5%, 실습/숙제: 15%, 실습시험: 25%, 중간: 25%, 기말: 30% 참고. 바뀔 수도 있음 프로그래밍 숙제(pre-Lab, post-Lab)는 수업 날짜 하루 전 밤 12시까지 전자우편으로 제출되어야 인정됨 전자우편의 제목은 반드시 다음과 같은 형태로 해야 함. 김상진, 자료구조, 실습 1 post-Lab, 실습 2 pre-Lab 일반 숙제는 수업시간 전에 제출되어야 인정됨 1시간 이상 지각할 경우 지각 처리함 합당한 사유를 사전에 통보하면 결석 처리하지 않음 결석할 경우에도 실습 내용을 숙제로 전부 제출해야 함
4/7
강의정보 – 강의 준비물 Pre-requiste C 언어, JAVA 언어를 알고 있어야 함. 특히 자바 언어를 이용하여 강의하므로 자바 언어는 필수 강의 준비물 교재, 이론 노트, 실습 노트를 강의 홈페이지에서 다운받아 준비 해야함. 실습 노트: 실습 설명, pre-Lab, in-Lab, post-Lab으로 구성 pre-Lab은 수업에 들어오기 전에 해서 가지고 와야 함 in-Lab은 실습 시간에 수행함 post-Lab은 숙제로 수행함 실습에서 자바 프로그래밍을 위해 자바 서적 지참 가능
5/7
강의정보 – 자바 관련 추천 도서 Cay S. Horstmann, Core Java 2 Vol. 1: Fundamentals, 5th Ed., Prentice Hall, 2000. H.M. Deitel and P.J. Deitel, JAVA: How to program, 5th Ed., Prentice Hall, 2003. W. Savitch, Absolute Java, Addison Wesley, 2004. J. Gosling et al., The Java Language Specification, 2nd Ed., Addison Wesley, 2000. 마에바시 가즈야, 자바를 지배하는 핵심원리, 영진.COM, 2003.
6/7
강의 계획 자바를 기반으로 강의 및 실습 1
교과목 소개
9
2
소프트웨어공학 및 자료구조 개요
10 연결구조 확장
3
자바 복습
11 재귀 프로그래밍
4
배열, 벡터
12 이진 검색 트리
5
자바를 이용한 범용 자료구조의 구현
13 실습시험
6
리스트
14 힙, AVL 트리
7
스택, 큐
15 그래프
8
중간시험
16 2-3 트리, 2-3-4 트리
연결구조
7/7
자료구조 및 실습(INA240) NOTE 01
©copyright 2005
소프트웨어공학과 자료구조 개요 한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 소프트웨어 개발 절차 고품질 소프트웨어의 목표 소프트웨어 설계 방법론 객체지향 설계 방법론 소프트웨어 정확성 검증 방법 자료구조 개요
2/29
소프트웨어 절차 소프트웨어의 크기가 커지고 복잡해질수록 코딩 외에 다른 소프트웨어 이슈에 대해 관심을 가져야 한다. 팀 프로젝트 수행시 더욱 중요 소프트웨어 개발의 단계 문제 분석: 타당성 조사, 성격, 범위, 목적 요구사항 분석: 사용자 및 시스템 요구사항 소프트웨어 명세서: not how but what 설계 위험 분석 구현 검사와 검증 운영 소프트웨어 생명주기(lifecycle) 유지보수 not sequential!!! 3/29
소프트웨어 절차 – 계속 문제/요구사항 분석할 때 고려 사항 입력의 형태, 데이터의 유효성 판단 기준, 소프트웨어를 사용할 사용자 유형, 사용자 인터페이스의 형태, 예외적인 특수한 경우의 존재 여부, 미래에 추가될 기능 개발할 소프트웨어의 프로토타입(prototype) 프로토타입 또는 일부 구성요소의 프로토타입을 만들면 관련 사람들간에 커뮤니케이션을 향상시킬 수 있으며, 위 문제에 대한 보다 정확한 답을 얻을 수 있다.
prototyping에 의한 소프트웨어 개발 방법론
4/29
소프트웨어 공학 소프트웨어 공학(software engineering): 고품질의 소프트웨어 공학 개발의 모든 측면과 관련된 학문을 말함 사용되는 기술과 행위(문서화, 팀워크 등)를 모두 포함 소프트웨어 프로세스(software process): 시스템을 개발하기 위해 프로세스 개인 또는 조직이 사용하는 소프트웨어 공학 기술의 집합을 말함 사용하는 소프트웨어 기술 또는 도구 하드웨어 소프트웨어: 컴파일러, 디버거 등 아이디어웨어: 소프트웨어 기술과 관련된 지식
5/29
고품질 소프트웨어의 목표 동작해야 한다. 고품질 SW는 그것의 임무를 정확하고 완전하게 수행해야 한다. 완전성, 정확성, 사용의 편리성, 효율성 많은 시간과 노력을 하지 않고 수정할 수 있어야 한다. 소프트웨어의 모든 단계에서 수정이 필요할 수 있음 유지보수: 기능의 추가, 검사 과정에서 발견하지 못한 오류 수정이 용이하기 위한 조건 읽고 이해할 수 있어야 한다. Æ 문서화 다룰 수 있는 정도의 크기로 나뉘어져 있어야 한다. Æ 모듈화 재사용이 가능해야 한다. 요구된 시간 내에 그리고 책정된 비용을 초과하지 않고 완성되어야 한다.
6/29
문제의 이해 프로그래밍 숙제에 대한 설명으로 분량이 12페이지인 문서가 학생들에게 제시되었다. 기간은 일주일이 주어졌다. 다음은 이것에 대한 학생들의 반응을 조사한 결과이다. Panic and do nothing 39% Panic and drop the course 30% Sit down at the computer and begin typing 27% Stop and think 4%
7/29
문제의 이해 – 계속 자세한 문제의 명세서 작성 요령: 시나리오를 생각해본다. 시나리오: 프로그램을 실행하였을 때 일어날 수 있는 일련의 사건 예1.1) ATM 기계 고객은 은행 카드를 삽입한다. ATM은 카드로부터 계좌번호를 알아낸다. ATM은 PIN 번호를 요청하면 고객은 PIN 번호를 입력한다. ATM은 PIN 번호를 확인한다. ATM은 거래 종류(입금, 이체, 조회, 종료)를 물어본다. 고객은 조회를 선택한다. ATM은 계좌의 현재 잔액을 읽고 그것을 나타내준다. ATM은 거래 종류를 다시 물어본다. 고객은 종료를 선택한다. ATM은 은행카드를 되돌려준다. 8/29
소프트웨어 설계 추상화(abstraction): 특정 관찰자 입장에서 시스템의 필수적인 추상화 사항만을 포함하는 복잡한 시스템에 대한 모델 관찰자마다 다를 수 있음 불필요한 하위 수준의 세부사항에 대한 고려 없이 적절한 수준에서 문제에 대해 집중할 수 있도록 해준다. (stepwise-refinement) 추상화된 것은 점진적으로 세분화되어 구체화된다. 하향식(top-down) 상향식(bottom-up) 객체지향 접근: 상향식에 가까운 접근 방법 분할정복(divide-and-conquer): 복잡한 문제일 수록 한 덩어리로 분할정복 접근하는 것이 매우 어렵다.
9/29
소프트웨어 설계 – 계속 전체 문제의 작은 일부분을 모듈(module)이라 한다. 모듈 정보 은닉(information hiding): 모듈은 그것의 복잡한 내부 구조를 은닉 외부로부터 숨긴다. 복잡성을 줄이기 위한 목적 모듈은 그것의 인터페이스를 통해서만 다른 모듈과 상호작용한다. 모듈의 요구사항 느슨한 결합성(loosely-coupled): 모듈은 상호 독립적이어야 결합성 한다. 한 모듈의 변경이나 오류가 다른 모듈에 영향을 주지 않아야 한다. 높은 응집성(highly-cohesive): 잘 정의된 단일 작업만해야 한다. 응집성
10/29 10/29
소프트웨어 설계 – 계속 각 모듈의 설계할 때 중요한 고려사항 모듈 간에 데이터 흐름(data flow) 흐름 소프트웨어 내에서 데이터 흐름을 명확하게 하기 위해서는 각 모듈마다 다음이 고려되어야 한다. (input) input 모듈을 실행하기 전에 제공될 수 있는 데이터는? (assumption) assumption 모듈이 어떤 것을 가정하고 있는가? (output) output 모듈의 행동에 따라 모듈이 실행된 후에 데이터의 모습은? 후에 설명하는 사전/사후조건과 연관되며, 팀 프로젝트에서는 매우 중요한 통신 수단이 된다. 비고. 이런 흐름은 모듈의 내부의 처리 방법과는 무관하다.
11/29 11/29
객체지향 설계와 구조화된 설계의 차이점 객체지향 설계(object-oriented design)는 문제에서 객체를 식별하고, 설계 이 객체들을 이용하여 모델링한다. (명사) 데이터 중심 실세계와 가깝게 모델링할 수 있음 구조화된 설계(structured design)는 문제에서 행위를 식별하여 설계 행위 위주로 모델링한다. (동사) 알고리즘 중심
객체지향 프로그래밍의 세가지 핵심 요소 1. 캡슐화(encapsulation): 데이터와 행위의 결합 2. 상속(inheritance): 코드의 재사용 3. 다형성(polymorphism): 실행시간에 객체에 의해 행위가 결정됨 12/29 12/29
객체지향 설계 클래스를 발견하는 방법 문제 정의에서 명사와 동사를 찾는다. 명사는 객체로 동사는 객체의 행위로 보통 모델링된다. 보통 주요 클래스를 발견하는 것은 어렵지 않다. 추상화를 사용하여 우선 자세한 세부사항은 고려하지 않는다. 여러 사람들과 브레인스토밍(brainstorming)을 한다. 여러 시나리오를 생각해본다. 발견한 클래스에 대해서는 가능하면 클래스를 상속 계층구조로 조직화한다. 각 클래스는 오직 하나의 주요 책임만을 가지도록 한다.
13/29 13/29
객체지향 설계 예) 문제의 정의: 주소록 책 작성 잠재적 객체 겉장 항 페이지 이름 집전화번호 전화번호 회사전화번호 팩스번호 휴대전화번호
생년월일 주소 집주소 회사주소 사용자 요구사항 회사이름 - 필요없다고 판단 달력 소유자 정보 사용자 Æ 사용자 인터페이스
14/29 14/29
객체지향 설계 예) 계속 각 객체마다 CRC(Class, Responsibility, Collaboration) 카드를 작성 클래스 이름: entry
부모 클래스:
자식 클래스:
주요 책임: 주소록 한 항에 대한 정보를 관리한다. 책임
협동
전체 이름을 하나의 문자열로 제공
name 클래스로부터 성을 name 클래스로부터 이름을
주소 제공
없음
15/29 15/29
객체지향 설계 예) 계속 시나리오 검토 사용자가 주소록에서 주소를 찾고 싶다. 사용자 인터페이스는 사용자로부터 이름을 입력받아야 한다. 어디서 찾나? Æ 주소록 전체를 나타내는 객체가 필요함 비교를 해야 한다. 어디서? Æ 주소록 or 항 항에서 이름을 받아 사용자가 입력한 이름과 비교한다. 일치하는 것을 찾으면 그 항으로부터 주소를 전달받는다. 다른 시나리오 검토 새 항의 추가, 기존 항의 삭제 등
16/29 16/29
소프트웨어의 정확성 검증 테스팅(testing): 오류를 발견하기 위해 설계된 데이터 집합을 이용하여 프로그램을 실행하는 과정 디버깅(debugging): 발견된 오류를 제거하는 과정 승인 시험(acceptance test): 실제 데이터를 이용하여 실제 환경에서 시스템을 테스트하는 과정 오류는 소프트웨어 개발 단계에서 일찍 발견할수록 수정 비용이 저렴하다. 명세서 또는 설계 오류가 가장 치명적이다.
17/29 17/29
구현 오류 컴파일 시간 오류(compile-time error): 문법 오류 실행 시간 오류(run-time error) 잘 못된 가정: result = dividend / divisor 사용자 입력 오류 논리 오류 오류가 발생하였을 때 그것을 극복할 수 있는 능력을 강건성 (robustness)이라 한다. 예외 처리(exception handling) 설계부터 어떤 가능한 예외 상황이 발생할 수 있는지 고려해야 함 what, where, how
18/29 18/29
정확성을 위한 설계 사전조건(precondition): 사후조건이 보장되기 위해 메소드에 사전조건 진입하기 전에 반드시 만족해야 하는 가정 메소드를 호출하는 사용자의 책임 사전조건이 위배된 경우에는 어떻게? 방법1. 예외 처리 방법2. 아무것도 하지 않는다. 사후조건(postcondition): 사전조건이 충족되었을 때, 사후조건 기대되는 메소드의 실행 결과 어떻게 그 결과를 얻는지는 중요하지 않음 메소드를 구현하는 개발자의 책임 사전, 사후조건에 대한 올바른 이해가 없으면 여러 가지 논리 오류를 범할 수 있다.
19/29 19/29
정확성을 위한 설계 – 계속 사후조건의 종류 종류 1. 반환 값의 정확성 종류 2. 객체의 상태 사전, 사후조건의 예) void RemoveLast() 효과: 리스트에 있는 마지막 요소를 제거한다. 사전조건: 리스트가 비어있지 않아야 한다. 사후조건: 리스트에 있는 마지막 요소가 제거되어 있다. WARNING If you try to execute this operation when the preconditions are not true, the results are not guaranteed
20/29 20/29
Deskchecking – Design 각 클래스의 기능과 목적이 명확한가? 큰 클래스를 세분화할 수 없는가? 공통된 코드를 공유하는 클래스들이 있는가? 있으면 이들을 상속 계층구조를 이용하여 나타낼 수 있는가? 상속은 코드 재사용이 목적이지만 이 목적만으로 상속하는 것은 옳지 않다. 즉, 논리적으로도 상속 관계가 성립해야 한다. 모든 가정이 합당하며, 문서에 잘 나타나 있는가? 모든 사전/사후조건이 정당한가? 명세서와 비추어 봤을 때 설계가 완전하고 정확한가?
21/29 21/29
Deskchecking – Coding 프로그래밍 언어의 기능을 올바르게 사용하고 있는가? 설계에 나타난 인터페이스와 일관성 있게 메소드들이 구현되었는가? 메소드 호출의 실제 인수와 메소드 정의에 선언된 파라미터가 일치하는가? 각 데이터 값들의 초기화가 제대로 되어 있는가? 모든 루프가 종료하는가? 매직 값들은 없는가? (매직 값은 값 자체를 보면 그 의미를 알기 어려운 값 Æ 명명된 상수 사용) 각 상수, 클래스, 변수, 메소드의 이름이 적절한가?
22/29 22/29
테스팅 – 단위 테스팅 블랙 박스 테스팅 (데이터 위주 테스팅) 기능 영역: 유효한 입력의 집합 기능 영역이 작으면 모든 경 우를 검사할 수 있다. 랜덤 검사: 기능 영역에서 몇 개의 입력을 랜덤하게 선택하여 검사하는 방법 기능 영역을 분류하여 각 소 영역 별 하나의 입력을 검사 하는 방법. 예1.2) 입력이 정수: 음수, 0, 양수
투명 박스 테스팅 (코드 위주 테스팅) 메소드의 각 문장을 차례로 실행하면서 검사하는 방법 분기(branch): 항상 실행 되지 않는 일련의 문장 경로(path): 메소드가 실행 되었을 때 실행될 수 있는 분기의 조합 경로 검사(path testing): 모든 경로를 다 실행하면서 검사하는 방법
23/29 23/29
루프의 정확성 검증 루프 불변조건(loop invariant): 루트가 시작되기 전, 루프가 반복된 불변조건 후, 루프가 종료된 후에 항상 만족되어야 하는 조건을 말한다. 추가적으로 알고리즘의 정확성을 충족시켜야 한다. 예1.3) 배열 item에 있는 요소들의 합 구하기 int sum = 0; int i = 0; while(i<n){ sum += item[i]; i++; }
루프 불변조건. sum은 item[0]부터 item[i-1]까지의 합이어야 한다.
24/29 24/29
데이터, 데이터 타입 데이터: 데이터 프로그램에 의해 처리 되는 정보 보통 데이터라 하면 값(value)를 의미하는 경우가 많음 데이터 타입: 타입 다음 두 가지에 의해 정의된다. 이 데이터 타입이 표현할 수 있는 요소들의 집합. 예1.4) 정수 이 요소들에 적용할 수 있는 연산의 집합. 예1.5) 사칙연산 프로그래밍 언어는 두 종류의 데이터 타입을 제공한다. 시스템 정의 데이터 타입 단순 타입(원자 타입, 원시 타입): 더 이상 분해할 수 없는 타입 타입 복합 타입: 타입 여러 요소로 구성되어 있는 타입 Æ 요소 접근 연산 구조화된 타입과 비구조화된 타입 사용자 정의 데이터 타입
25/29 25/29
추상화 절차의 추상화(procedural abstraction) 추상화 메소드의 목적과 그것의 구현을 분리 데이터 추상화(data abstraction, encapsulation) 추상화 데이터에 가능한 연산과 데이터 저장 방법 및 연산 구현을 분리 추상 데이터 타입(ADT, Abstract Data Type) 타입 특정 구현과 무관하게 특성이 명시된 데이터 타입 프로그래밍 언어에서 제공하는 int 타입 역시 ADT로 볼 수 있다. 그것의 내부 구현을 몰라도 사용할 수 있다.
26/29 26/29
자료구조 자료구조(data structure) 자료구조 정의1. 데이터를 저장하는 방법과 관련된 것으로서 데이터 요소들 의 모음을 모음 말한다. 이 모음의 논리적 구성은 개별 요소 간에 논리적 관계를 관계 나타낸다. 정의2. 데이터의 모음을 저장하기 위해 프로그래밍 언어를 이용하여 정의할 수 있는 구조 자료구조는 개별 데이터 요소를 검색하고 저장하기 위해 사용되는 접근 연산에 의해 특징지어진다. 특징 자료구조는 ADT로 구현된다.
27/29 27/29
자료구조 – 계속 자료구조의 특성 구성 요소로 분해될 수 있다. 요소들이 조직되어 있는 형태는 각 개별 요소에 대한 접근 방법에 영향을 준다. 조직되어 있는 형태와 접근 방법을 모두 캡슐화할 수 있다. 자료구조에서 제공하는 기본 연산의 분류 생성자: 생성자 새 인스턴스를 생성할 때 사용되는 연산 기존 객체의 내용을 이용하여 새 객체를 생성하는 생성자를 복사 생성자(copy constructor)라 한다. 수정자: 수정자 데이터의 값들의 상태를 변경할 때 사용되는 연산 관찰자: 관찰자 데이터의 값들의 상태를 열람할 때 사용되는 연산 반복자(iterator): 데이터 구조에 있는 모든 구성요소를 순차적으로 반복자 처리할 수 있도록 해주는 연산
28/29 28/29
알고리즘과 데이터 알고리즘(algorithm): 유한 단계 내에 문제를 해결하는 방법을 알고리즘 단계별로 기술한 것을 말한다. 알고리즘과 데이터는 서로 매우 밀접하게 관련되어 있음 자료구조에 따라 그것에 적용할 수 있는 알고리즘이 달라진다. 데이터를 정렬된 상태로 유지하고 임의 접근을 제공하는 구조는 이진 검색이 가능하다.
29/29 29/29
©copyright 2005
자료구조 및 실습(INA240) NOTE 02
자바 배열 복습
한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 자바 배열 ArrayList Vector
2/22
배열
z z z
용량: 10 크기: 3
배열의 특성 동질 구조(homogeneous structure): 구조에 있는 모든 요소는 구조 같은 타입이다. 예2.1) 일차원 배열에서 첫 슬롯에 배열에 저장되어 있는 요소들의 개수를, 나머지 슬롯에 정수값을 저장한 배열 물리적으로 동질 구조이지만 논리적으로는 동질 구조가 아님 요소들간에 순서가 존재한다. 존재 배열의 요소는 위치에 의해 접근된다. (index: 0부터 시작) 배열의 용량은 컴파일 시간에 정해진다. 정해진다 배열의 모든 슬롯에 유효한 요소가 들어있을 필요는 없다. 배열의 용량을 변경할 수 없다. 임의 접근 제공: 제공 모든 요소를 바로 접근할 수 있다. 용량이 고정되어 있기 때문에 임의 접근이 가능하다. 3/22
일차원 배열 자바에서 배열은 참조 타입이다. 일차원 배열의 선언 예2.2) int[] numbers; // reference type C/C++처럼 int numbers[];와 같이 선언될 수도 있다. 다른 참조 타입과 마찬가지로 new를 이용하여 생성되어야 한다. 예2.3) numbers = new int[10]; C/C++처럼 int numbers[10];과 같이 new를 사용하지 않고, 선언과 동시에 생성하는 것은 가능하지 않다. 선언과 생성을 동시에 할 수 있다. 예2.4) int[] numbers = new int[10]; new를 이용하여 생성된 배열의 초기값은? 기본 값으로 자동으로 초기화된다.
4/22
일차원 배열 – 계속 배열의 요소 접근: [] 연산자 이용 예2.5) numbers[2] = 5; value = numbers[i]; 색인값이 0에서 9사이의 값이 아니면 자바는 예외를 발생시킨다. 예외: ArrayIndexOutofBoundException 배열의 용량은 public 멤버변수인 length를 이용한다. 예2.6) numbers.length 사용자는 이 변수의 값을 변경할 수 없다. 예2.7) numbers.length = 10; 비고. String에서 문자열의 길이는 length() 메소드를 이용 numbers length
5/22
일차원 배열 – 계속 초기값을 제공하여 생성할 수 있다. 예2.8) int[] numbers = new int[] {5, 7, 4, 3, 10, 22, -6, -3}; 이 때 numbers의 용량은 8이다. 다음과 같이 new int[] 부분을 생략할 수 있다. 예2.9) int[] numbers = {5, 7, 4, 3, 10, 22, -6, -3}; 대입문을 이용하여 초기화하는 것보다 초기값을 이용하는 것이 더 효율적이다. int[] age = {2, 12, 10}; 이 형태가 더 효율적
int[] age = new int[3]; age[0] = 2; age[1] = 12; age[2] = 10;
예2.8) int[] numbers = new int[8] {5, 7, 4, 3, 10, 22, -6, -3};
6/22
문자 배열과 문자열 문자 배열은 String 타입의 객체가 아니다. 예2.10)
void f1(String s){ … } void f2(){ char[] fruit = {‘a’, ‘p’, ‘p’, ‘l’, ‘e’}; String s = fruit; // error f1(fruit); // error }
다음과 같은 문자 배열을 문자열로 변환할 수 있다. 예2.11) char[] fruit = {‘a’, ‘p’, ‘p’, ‘l’, ‘e’}; String s1 = new String(fruit); String s2 = new String(fruit, 2, 3);
이 처럼 String도 new 연산자를 사용하여 생성할 수 있지만 보통 new를 생략한다. 물론 이 경우에는 생략할 수 없다.
// ok // ok “ple”
문자 배열은 문자열처럼 출력할 수 있다. 예2.12) char[] fruit = {‘a’, ‘p’, ‘p’, ‘l’, ‘e’};
결과: apple
System.out.println(fruit);
7/22
배열의 복사 numbers 두 배열 변수가 같은 타입이면 상호 대입할 수 있다. 예2.13) int[] numbers = {1, 2, 3, 4}; int[] values = numbers; values는 numbers와 같은 배열을 참조하게 된다. values 한 배열에 있는 모든 값을 다른 배열로 복사하고 싶으면 System.arraycopy 메소드를 이용한다. 예2.14) int[] numbers = {1,2,3,4,5}; int[] values = {11,12,13,14,15}; System.arraycopy(numbers,0,values,2,3);
numbers
1 2 3 4 5
values
11 12 13 14 15
numbers
1 2 3 4 5
values
System.arraycopy(소스배열, 소스배열의 시작위치, 목적배열, 목적배열의 시작위치, 복사할 요소의 개수)
1 2 3 4
11 12 1 2 3
8/22
파라미터로 배열의 전달 자바에서 배열은 참조 타입이므로 인자로 메소드에 전달하여 메소드 내에서 배열을 조작하면 메소드가 끝난 후에도 조작 결과가 계속 유지된다. void f1(int[] a){ 예2.15) a[2] = 10; } 결과: 1,2,10,4 void f2(){ int[] A = {1,2,3,4}; f1(A); void f1(int[] a){ for(int i=0; i<4; i++) int[] B = {5,6,7,8}; System.out.println(A[i]); a = B; 결과: 1,2,3,4 } }
하지만 이 역시 call-by-value 형태로 전달되는 것이다. 예2.16)
void f2(){ int[] A = {1,2,3,4}; f1(A); for(int i=0; i<4; i++) System.out.println(A[i]); } 9/22
Call-by-value와 참조 변수 public class A{ private int value; public void set(int n) { value = n; } public int get() { return value; } } // class A public class B{ public static void f(A b){ A c = new A(); b.set(10); b = c; } // f public static void main(String[] args){ A a = new A(); a.set(5); f(a); System.out.println(a.get()); } // main 결과: 10 } // class B
a
value v 10 5
b
c value
v
10/22 10/22
배열을 반환하는 메소드 배열을 메소드의 결과로 반환할 수 있다. 예2.17) int[] doubeTheSize(int[] a){ 2 int[] b = new int[a.length*2]; 3 System.arraycopy(a,0,b,0,a.length); 4 return b; void f(){ } int[] A = {1,2,3,4}; 1 2 5 A = doubleTheSize(A); 6 A[4] = 5; b for(int i=0; i<5; i++) System.out.println(A[i]); } 3
a
A 1 2
5
1 2
4
3 4
1
2
3
4
1
2
3
4
6
5 11/22 11/22
객체 배열 예2.18) Circle[] allCircles = new Circle[5]; Circle 객체에 대한 참조를 5개까지 저장할 수 있는 배열이 생성된다. 5개의 Circle 객체가 생성된 것은 아니다. 이 때 각 항은 null로 초기화된다. 각 개별 객체를 생성하고자 하면 추가로 다음과 같은 코드를 실행해야 한다. for(int i=0; i<allCircles.length; i++){ allCircles[i] = new Circle(); }
12/22 12/22
final과 배열 예2.19)
void f(){ final int[] numbers = new int[5]; numbers[3] = 4; // ok numbers = new int[3]; // error }
final int[] numbers = new int[5]에서 final은 numbers와 연관이 있는 것이지 배열 자체와 있는 것은 아니다.
이 부분만 final
13/22 13/22
다차원 배열 2차원 배열은 행과 열로 구성된 테이블을 나타내기 위해 사용한다. 2차원 배열의 선언 예2.20) double[][] alpha = new double[100][10]; 첫 번째 [100]은 행을 두 번째 [10]은 열을 나타낸다. 2차원 배열의 접근 alpha[0][5] = 36.4; 행과 열의 개수 행의 개수: alpha.length 열의 개수: alpha[i].length
14/22 14/22
다차원 배열 – 계속 일반적으로 2차원 배열의 각 행의 용량은 같도록 만든다. 예2.21) int[][] a = new int[10][5]; 그러나 각 행의 용량이 다를 수도 있다. 예2.22) int[][] a = new int[2][]; a[0] = new int[5]; a[1] = new int[3];
15/22 15/22
ArrayList ArrayList는 java.util 패키지에 정의되어 있는 클래스로 배열과 유사하지만 배열과 달리 용량이 고정되어 있지 않다. 않다 즉, 필요에 따라 용량이 변한다. ArrayList는 size() 메소드를 이용하여 현재 저장되어 있는 요소의 개수를 얻을 수 있다. 배열은 특정 타입을 저장하도록 선언할 수 있지만, ArrayList는 Object 타입만 저장할 수 있다. (참조를 저장한다.) 따라서 원시 타입은 wrapper 클래스를 사용해야 한다. 이론적으로 서로 다른 종류의 값을 ArrayList의 요소로 사용 가능 int[] a = new int[10]; a[0] = 3; a[1] = 2.5; // error
ArrayList a = new ArrayList(); a.add(new Integer(10)); a.add(new Double(2.5)); 바람직한 프로그래밍 스타일은 아니다. 16/22 16/22
ArrayList – 계속 ArrayList는 클래스이며, 자바는 연산자를 재정의할 수 없으므로 배열과 달리 [ ] 연산자를 이용하여 요소를 접근할 수 없다. ArrayList도 배열과 마찬가지로 색인을 이용하여 저장되어 있는 객체들을 접근할 수 있다. 색인의 시작은 배열과 마찬가지로 0이다. add()와 get() 메소드를 이용한다. void add(Object element): 맨 끝에 요소를 추가한다. void add(int index, Object element): 주어진 색인 위치에 요소를 추가한다. 이 때 색인 위치에 요소가 이미 있으면 그 요소부터 모든 요소는 오른쪽으로 하나씩 이동된다. Object get(int index): 색인 위치에 있는 요소를 반환한다. 만약 index가 유효한 색인(0≤index<size())이 아니면 IndexOutOfBoundsException을 발생한다.
17/22 17/22
ArrayList – 계속 항상 ArrayList의 오른쪽은 비어있다. (왼쪽정렬 방식) 중간에 빈 슬롯이 있을 수 없다. 없다 Object set(int index, Object element): 색인 위치에 있는 요소를 주어진 객체로 대체한다. 또한 결과로 이전에 그 위치에 있던 객체를 반환한다. 만약 index가 유효한 색인이 아니면 IndexOutOfBoundsException을 발생한다. Object remove(int index): 색인 위치에 있는 요소를 삭제하고, 그 뒤 에 있는 객체들을 하나씩 왼쪽으로 이동한다. 또한 결과로 삭제한 객체를 반환한다. 만약 index가 유효한 색인이 아니면 IndexOutOfBoundsException을 발생한다. void trimToSize(): ArrayList의 용량을 현재의 크기로 축소한다.
18/22 18/22
ArrayList – 계속 add/remove 메소드 사용시 주의점 add는 최악의 경우 현재 저장되어 있는 모든 요소를 하나씩 오른쪽으로 이동해야 한다. 반대로 remove는 최악의 경우 현재 저장되어 있는 모든 요소를 하나씩 왼쪽으로 이동해야 한다. 현재 크기가 용량과 같은 경우에는 현재보다 큰 용량의 배열을 생성한 다음에 기존 배열에서 요소들을 복사한다. get 메소드 사용시 주의점 반환타입이 Object이므로 반환값을 원하는 타입으로 강제 변환 해주어야 한다. ArrayList의 생성자 ArrayList() : 용량이 10인 빈 리스트를 생성한다. ArrayList(int capacity): 주어진 인자에 해당하는 용량의 빈 리스트를 생성한다.
19/22 19/22
반복자 ArrayList는 반복자(iterator)를 가지고 있다. 반복자란 구성요소를 차례로 방문할 때 사용하는 도구를 말한다. 반복자는 기본적으로 다음 두 연산을 제공한다. boolean hasNext(): 더 이상의 요소가 있는지 확인할 때 사용 Object next(): 다음 요소를 얻기 위해 사용 예2.23) void printVector(ArrayList list){ Iterator i = list.iterator(); while(i.hasNext()){ System.out.println(i.next()); } }
20/22 20/22
단순 배열 vs. ArrayList 단순 배열 공간 문제가 중요하지 않는 경우 실행시간이 중요한 경우 요구되는 배열의 용량이 프로그램 실행마다 크게 변하지 않는 경우 배열의 크기가 프로그램이 실행되는 동안 변하지 않는 경우 배열에 있는 요소의 위치가 중요한 경우
ArrayList 공간 문제가 중요한 경우 실행시간이 중요하지 않는 경우 요구되는 배열의 용량이 프로그램 실행마다 크게 변하는 경우 배열의 크기가 프로그램이 실행되는 동안 극단적으로 변하는 경우 배열에 있는 요소의 위치가 중요하지 않는 경우 삭제/삽입이 대부분 끝에 이루어지는 경우 21/22 21/22
Vector java.util 패키지에 포함되어 있으며, ArrayList와 그 기능이 매우 유사하다. 가장 중요한 차이점은 Vector는 다중쓰레드를 지원하고 ArrayList는 지원하지 않는다. 따라서 다중쓰레드 환경이 아니면 Vector 대신에 ArrayList를 사용하는 것이 바람직하다. ArrayList와 달리 capacity() 메소드를 이용하여 현재 용량을 얻을 수 있다. Vector에 더 이상 요소를 삽입할 공간이 없으면 지정해 놓은 용량만큼 또는 기본 증가 용량만큼 자동으로 증가한다. 기본 증가 용량은 현재 용량의 두 배이다. Vector() Vector(int capacity) 증가하는 용량을 지정하고 싶으면 벡터를 생성할 때 해야 한다. Vector(int capacity, int increment)
22/22 22/22
©copyright 2005
자료구조 및 실습(INA240) NOTE 03
자바를 이용한 범용 자료구조의 구현 한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 자바를 이용한 범용 자료구조의 구현 Object 클래스 equals 메소드 clone 메소드 Object 클래스를 이용한 범용 자료구조의 구현 compareTo 메소드 복사 방식 vs. 참조 방식 clone 메소드 Listable 인터페이스
2/26
범용 자료구조 – 개요 (1) 범용 자료구조란 특정한 데이터 타입만 저장할 수 있는 구조가 아닌 모든 종류의 데이터를 저장할 수 있는 구조를 말한다. 예3.1) Point 데이터만 저장할 수 있는 PointList ADT는 범용 자료구조가 아니다. 범용 자료구조를 자바에서 구현하기 위해서는 다양한 종류의 데이터를 모두 저장할 수 있는 데이터 타입이 필요하다. 이 때 가장 쉽게 생각할 수 있는 것은 Object 클래스이다. 클래스 Object 클래스는 자바에서 가장 최상위 조상 클래스이며, 명백하게 부모를 지정하지 않은 클래스의 부모는 항상 자동으로 Object 클래스가 된다. 객체는 그것의 조상 클래스 타입 변수에 대입할 수 있다. 따라서 모든 객체는 Object 타입의 변수에 저장할 수 있다.
3/26
범용 자료구조 – 개요 (2) Object 타입을 이용한 범용 자료구조 구현의 문제점 문제점 1. 원시타입은 Object 타입에 저장할 수 없다. 해결책. Wrapper 클래스를 사용한다. 문제점 2. 자료구조마다 그 자료구조에 저장되는 요소가 반드시 제공해야 하는 메소드가 있을 수 있다. 예3.2) 정렬리스트의 경우 저장되는 두 요소의 순위를 비교하는 메소드가 필요함. 예3.3) 자료구조를 만들 때 복사 방식 또는 참조 방식 두 가지 방식으로 사용하여 구현할 수 있다. 이 때 복사 방식을 사용하면 객체를 복제하는 메소드가 필요함 Object 타입을 사용할 경우에는 강제 타입 변환을 하지 않으면 이 클래스에 정의되어 있는 메소드만 호출할 수 있다. Object 타입에 필요한 메소드가 정의되어 있지 않을 수 있다.
4/26
Object 클래스 자바에서 다른 클래스를 명백하게 상속받지 않는 경우에는 자동으로 Object 클래스를 상속받는다. 이 클래스는 상속 계층구조 상에서 가장 최상위 조상 클래스이다. Object 클래스는 toString, equals, clone과 같은 유용한 메소드를 제공한다. 이 중 범용 자료구조를 만들 때 많이 사용되는 메소드는 equals와 clone이다. equals 메소드는 두 객체의 내부 상태가 같은지 비교할 때 사용된다. clone 메소드는 상태가 같은 다른 객체를 생성할 때 사용된다. 두 메소드를 재정의(override)하지 않으면 Object 클래스에 정의되어 있는 메소드가 호출된다.
5/26
equals 메소드 Object의 equals 메소드의 문제점 Object 클래스에 정의되어 있는 equals 메소드: 두 객체 변수가 같은 객체를 가리키고 있는지 검사한다. 우리가 원하는 equals의 효과는 두 객체의 내부 상태를 비교하는 것이다. 따라서 Object에 정의되어 있는 equals 메소드는 우리가 보통 원하는 효과를 얻을 수 없다. 따라서 클래스를 만들 때 보통 기본적으로 이 메소드는 재정의해야 한다.
6/26
clone 메소드 Object의 clone 메소드의 문제점 Object 클래스에 정의되어 있는 clone 메소드: 이 메소드는 같은 타입의 객체를 자동으로 생성하고 인스턴스 필드의 값을 자동으로 복사한다. (shallow copy) 클래스의 멤버 변수 중 참조 타입이 있으면 문제가 발생할 수 있다. 예3.4) public class logbook{ private int logMonth; private int logYear; private int entry = new int[31]; … }
logMonth
logMonth
logYear
logYear
entry
entry
7/26
boolean equals(Object other) equals 메소드의 구현 사전조건: 사전조건 비교하고자 하는 객체가 null이 아니어야 하며, 같은 클래스의 인스턴스이어야 한다. 사후조건: 사후조건 객체의 내부 상태를 비교하여 내부 상태가 같은 경우에는 true를 반환하고, 그렇지 않으면 false를 반환한다. 같은 클래스의 인스턴스인가를 검사하는 방법 instanceof 연산자: 가능하지만 하위 클래스도 이 연산자를 이용한 검사에서 통과하므로 이 경우에는 사용할 수 없다. Object 클래스의 Class getClass() 메소드 사용 추가적 고려사항 동일 객체인지 검사한다. Æ if(this==other) return true; 인수의 타입이 Object이므로 강제 타입 변환을 한 다음에 각 멤버변수를 비교해야 한다. 8/26
boolean equals(Object other) – 계속 예3.5) public class Date{ private int year; private int month; private int day; … public boolean equals(Object other){ if(other==null||getClass() != other.getClass()) return false; if(this == other) return true; Date o = (Date)other; public class MyDate extends Date{ return year==o.year&& private int val; month==o.month&& … day==o.day; public boolean equals(Object other){ } if(!super.equals(other)) return false; … MyDate o = (MyDate)other; } return val == o.val; } 서브 클래스의 경우 9/26
Object clone() Object 클래스의 clone 메소드는 객체의 멤버 변수 중에 참조 타입이 있으면 문제가 발생할 수 있다. 따라서 이 메소드는 protected로 선언되어 있다. 즉, 재정의하지 않으면 호출할 수 없다. clone을 재정의하는 경우에는 Cloneable 인터페이스를 구현한다.
멤버변수 중 참조 타입이 있으면 그 변수마다 그 변수의 clone을 호출한다.
public class Customer implements Cloneable{ private String name; private BankAccount account; … public Object clone(){ try{ Customer cloned = (Customer)super.clone(); cloned.account = (BankAccount)account.clone(); return cloned; } catch(CloneNotSupportedException e){ return null; } } } 10/26 10/26
protected package pack1; class A{ private int a; public A(int val){ a = val; } protected void f(){ System.out.println(“Ha Ha!!!”); } } class B extends A{ private int b; public B(int val){ super(val); b = val; } public void g(){ f(); } public void test(){ a = 10; // error } }
package pack2; class C extends A { private int c; public void test(){ A o1 = new A(10); o1.a = 20; // error o1.f(); // error B o2 = new B(20); o2.g(); // ok } } 상속한 클래스가 아닌 관련이 없는 다른 패키지에 있는 클래스의 경우에는 protected는 private과 그 효과가 같다.
11/26 11/26
Object 클래스를 이용한 범용 자료구조 요소들을 어떤 기준에 의해 정렬하지 않은 상태로 유지하는 자료구조의 경우에는 삽입은 비교적 쉽게 구현할 수 있으나, 삭제는 내부적으로 요소들이 어떻게 저장되어 있는지와 무관하게 항상 삭제하고자 하는 요소가 현재 구조 내에 있는지 검사해야 한다. Object 클래스를 이용하여 범용 자료구조를 구현할 때에 내부 자료구조 뿐만 아니라 인자로 전달되는 객체를 받기 위해 Object 타입의 파라미터 변수를 사용한다. Object 클래스에 equals 메소드가 정의되어 있으므로 이 메소드를 이용하여 삭제하고자 하는 요소가 있는지 검사할 수 있다. 주의. Object 타입의 변수에 실제로 저장되어 있는 객체에 equals 메소드가 재정의되어 있지 않으면 우리가 원하는 효과를 얻을 수 없다.
12/26 12/26
Object 클래스를 이용한 범용 자료구조 예3.6) public class UnsortedList{ Object[] list; int size = 0; public UnsortedList(int capacity){ list = new Object[capacity]; } public boolean delete(Object item){ for(int i=0; i<size; i++){ if(item.equals(list[i])){ … } } } … }
자바의 late binding 기능 때문에 이 경우 Object에 정의되어 있는 equals 메소드가 아니라 item이 참조하고 있는 객체에 정의되어 있는 equals 메소드가 실행된다.
13/26 13/26
Object 클래스를 이용한 범용 자료구조 요소들을 어떤 기준에 의해 정렬된 상태로 유지하는 자료 구조의 경우에는 삽입할 때 기존 요소들과 비교하여 삽입 위치를 결정해야 한다. 이런 비교는 자바에서는 보통 compareTo 메소드를 이용한다. Object 타입을 이용하여 범용 자료구조를 구현하고자 할 경우에는 Object 클래스에는 compareTo 메소드가 정의되어 있지 않으므로 예3.6처럼 쉽게 구현할 수 없다. Object 타입 변수에 저장되어 있는 객체에 compareTo 메소드가 정의되어 있어도 Object 타입 변수를 통해서는 compareTo 메소드를 호출할 수 없다. 강제 타입 변환을 하여 호출할 수 있지만 범용 자료구조 구현에서는 어떤 타입의 객체가 변수에 저장될지 모르기 때문에 가능하지 않다.
14/26 14/26
Object 클래스를 이용한 범용 자료구조 예3.7)
public class Test{ public static void main(String[] args){ SortedList list = new SortedList(10); list.insert(new Integer(10)); } }
public class SortedList{ Object[] list; int size = 0; public SortedList(int capacity){ list = new Object[capacity]; } public void insert(Object item){ for(int i=0; i<size; i++){ int comp = item.compareTo(list[i]); // error … } 이 예제에서 Integer 클래스는 } compareTo 메소드가 정의되어 … 있지만 SortedList의 add 메소드는 } 전달된 객체의 타입과 상관없이
item은 Object 타입이므로 item을 이용하여 compareTo를 호출할 수 없다. 15/26 15/26
해결책 1. public class SortedList{ Object[] list; int size = 0; public SortedList(int capacity){ list = new Object[capacity]; } public void insert(Object item){ Comparable x = (Comparable)item; for(int i=0; i<size; i++){ int comp = x.compareTo(list[i]); // ok … } 이 때 item이 Comparable 인터페이스를 구현하고 } 있지 않으면 ClassCastException이 발생한다. … }
16/26 16/26
해결책 2. public class SortedList{ Comparable[] list; // Object[] list; int size = 0; public SortedList(int capacity){ list = new Comparable[capacity]; } public void insert(Comparable item){ for(int i=0; i<size; i++){ int comp = item.compareTo(list[i]); // ok … } } … }
17/26 17/26
복사방식 vs. 참조방식 복사방식(by clone)
참조방식(by reference)
public void insert(Listable item){ public void insert(Object item){ list[size] = (Listable)item.clone(); list[size] = item; 구현 예 size++; size++; } } 위험성
외부에서 내부 내용을 불법적으로 조작할 수 없다.
외부에서 내부 내용을 불법적으로 조작할 수 있다.
속도
객체가 크고 복잡할수록 느리다.
일정하며, 빠르다.
공간
두 배로 소요된다.
공간이 추가로 필요 없다.
여기서 Listable은 clone 메소드를 public으로 선언하고 있는 인터페이스 타입이다.
18/26 18/26
참조 방식의 문제점 list
class MyClass{ int value; Myclass(int n){ value=n; } public void set(int n){ value=n; } public int get(){ return value; } … } UnsortedList list = new UnsortedList(5); MyClass a1 = new MyClass(10); MyClass a2 = new MyClass(20); list.insert(a1); list.insert(a2); a1.set(5);
10 5
a1
20
a2
19/26 19/26
복사 방식을 사용하는 경우 list
10 5
a1
20
10
20
a2
20/26 20/26
clone clone은 compareTo와 달리 Object 클래스에 존재한다. 하지만 Object의 clone 메소드는 protected 멤버이므로 외부에서는 호출할 수 없다. Object의 clone은 shallow copy(cf. deep copy)를 하기 때문에 객체의 멤버 중 참조 타입이 있으면 문제가 발생할 수 있다. 이 때문에 Object의 clone 메소드는 protected 멤버로 정의되어 있다. 그러면 Cloneable 인터페이스를 이용하여 compareTo 문제를 해결한 것처럼 해결할 수 있을까? 가능하지 않다. Cloneable 인터페이스는 내용이 비어있는 marker interface이다. 참고. marker interface는 그것을 구현하는 클래스의 특성을 나타낸다. 어떤 메소드를 제공한다는 것을 나타내지 않는다. 예3.8) Serializable: 객체를 파일에 저장할 수 있음을 나타낸다. 21/26 21/26
clone의 문제점 예3.9)
Cloneable 인터페이스가 clone 메소드를 선언하고 있으면 이 메소드가 public 메소드가 되어 호출이 가능하지만 marker interface이므로 가능하지 않다.
public class SortedList{ Object[] list; int size = 0; public SortedList(int capacity){ list = new Object[capacity]; } public Object get(int index) { // return list[index].clone(); Cloneable x = (Cloneable)list[index]; return x.clone(); // error; } … }
해결책. 새로운 인터페이스를 정의한다. interface Copyable{ Object clone(); }
public void get(int index) { Copyable x = (Copyable)list[index]; return x.clone(); // ok; } 22/26 22/26
clone과 compareTo 동시 제공 복사 방식으로 범용 자료구조를 구현하고 싶다. 그런데 이 자료구조는 추가적으로 특정 메소드가 제공되어야 한다. 어떻게 해야 하나? 예3.10) 복사 방식으로 정렬 리스트를 구현하고 싶다. 정렬 리스트를 구현하기 위해서는 compareTo 메소드가 제공되어야 한다. 방법. Comparable, Cloneable을 구현한 새 인터페이스를 다음과 같이 정의하여 사용한다. 참고. 인터페이스는 다중으로 인터페이스를 상속할 수 있다. interface Listable extends Comparable, Cloneable{ // int compareTo(Object target); Object clone(); }
23/26 23/26
clone과 compareTo 동시 제공 예3.11) Listable의 사용 예 public class SortedList{ Listable[] list; int size = 0; public SortedList(int capacity){ list = new Listable[capacity]; } public Object get(int index) { return list[index].clone(); } … }
public class SortedList{ Object[] list; int size = 0; public SortedList(int capacity){ list = new Object[capacity]; } public Object get(int index) { Listable x = (Listable)list[index]; return x.clone(); } … }
24/26 24/26
Listable 인터페이스 Listable 인터페이스의 문제점 자바 라이브러리에서 제공하는 Integer와 같은 기존 객체는 저장할 수 없다. 또한 기존 객체를 간단하게 Listable 제공하도록 다음과 같이 만들 수도 없다. class myInteger extends Integer implements Listable{ public myInteger(int val){ super(val); The type myInteger cannot subclass } the final class integer. }
위 문제점에 대한 효율적인 해결책이 없음. 복사 방식의 구현을 포기하거나, 아니면 저장할 객체마다 새롭게 Listable을 구현하도록 새 클래스를 정의함. 25/26 25/26
기타: 오류의 처리 계약 방식: 방식 사전 조건을 사용하는 측에서 검사하도록 하는 방법 이 경우에도 내부적으로 오류를 보통 검사한다. 아래 방식들과 차이점은 오류를 발견하였을 때 그 사실을 알려주지 않을 수 있다. 오류 직접 처리하는 방식: 방식 메소드를 구현할 때 오류를 검사하고 적절한 조치를 취하는 방식 오류 코드 반환 방식: 적절한 오류 코드를 반환하기가 어려울 수 있다. 오류 변수 방식: 오류 변수를 하나 정의하고 오류 코드를 변수에 저장하는 방식 사용자는 메소드를 사용한 후에 이 변수를 검사하여 오류 여부를 검사하는 방식 예외 처리
26/26 26/26
©copyright 2005
자료구조 및 실습(INA240) NOTE 04
리스트
한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 리스트 비정렬 리스트 정렬 리스트 알고리즘 비교 Big-O 표기법
2/25
리스트 리스트의 특성 동질 구조: 구조 구조에 있는 모든 요소는 같은 타입이다. 요소들간에 선형 관계가 존재한다. 존재 첫 번째 요소를 제외한 모든 요소는 선행 요소가 있으며, 마지막 요소를 제외한 모든 요소는 후속 요소가 있다. 리스트에 있는 요소의 개수를 리스트의 크기라 한다. 리스트는 정렬되어 유지될 수 있고, 그렇지 않을 수 있다. 비정렬(unsorted) 리스트, 정렬(sorted) 리스트 정렬 리스트에서 요소가 복합 타입일 경우에는 정렬의 기준이 되는 키가 존재한다. 예4.1) 학생 기록부: 학번, 학생 이름, 나이, ... Æ 키는 다양하게 결정될 수 있다.
3/25
리스트 ADT 구현시 고려사항 중복 허용 여부 동일한 것이 여러 번 삽입될 수 있는지 여부: 응용에 의해 결정 제공해야 하는 연산의 형태 보통 자료구조는 삽입, 삭제, 검색, 추출 연산을 제공한다. 각 연산의 형태는 정해져 있지 않으며, 응용에 따라 다를 수 있다. 예4.2) 삭제 주어진 키 값을 가진 요소를 삭제 자료구조 내에 특정 위치에 저장된 요소를 삭제
4/25
UnsortedList ADT 배열을 이용한 리스트의 구현 int size; capacity? int cursor; Object[] elements; 생성자 UnsortedList() UnsortedList(int capacity); 상태 boolean isFull(); boolean isEmpty(); int size(); 검색 boolean search(Object item); Object retrieve(Object item);
조작 boolean insert(Object item); boolean delete(Object item); 커서 조작 void reset(); iterator 커서를 처음으로 boolean hasNext(); 후속 요소가 있으면 true를 반환한다. Object next(); 커서가 가리키는 객체를 반환하고, 커서를 다음으로 이동한다.
5/25
boolean search(Object item) 사전조건: item!=null 사후조건: item이 리스트에 있으면 true를 반환하고, 없으면 false를 반환한다. 정렬되어 있지 않는 리스트 이므로 선형검색만 가능하다. 검색할 때 종료조건 1. 찾고자 하는 것을 발견함 2. 모든 요소와 비교하였지만 찾지 못함
boolean search(Object item){ int loc = 0; boolean moreToSearch = (item!=null)&&(loc<size); boolean found = false; while(moreToSearch && !found){ if(item.equals(elements[loc])){ found = true; } else{ loc++; moreToSearch = (loc < size); } } return found; } boolean search(Object item){ if(item==null) return false; for(int loc = 0; loc<size; loc++){ if(item.equals(elements[loc])) return true; return false; } 6/25
boolean insert(Object item) 사전조건: item!=null, 리스트가 full이 아님 사후조건: 주어진 item을 리스트에 끝에 추가한다. 추가에 성공하면 true를, 못하면 false를 반환한다.
boolean insert(Object item){ if(item==null || isFull()) return false; elements[size] = item; // Listable x = (Listable)item; // elements[size] = x.clone(); size++; return true; } item elements elements
item
7/25
boolean delete(Object item) 사전조건: 사전조건 item!=null 사후조건: 사후조건 item이 리스트에 있으면 그 item을 삭제한다. 삭제에 성공하면 true를 못하면 false를 반환한다. 검색할 때 종료조건은 search 연산과 같음. 하지만 이 경우에는 두 개의 boolean 변수를 사용하지 않음.
삭제할 것 A
D
C
F
A
D
E
F
E
boolean delete(Object item){ int loc = 0; boolean moreToSearch = (item!=null)&&(loc<size); while(moreToSearch){ if(item.equals(element[loc])){ if(loc!=size-1) elements[loc] = elements[size-1]; size--; 정렬되어 있지 않는 return true; 리스트이므로 가능 } else{ loc++; moreToSearch = (loc<size); } } // while return false; } // delete
8/25
boolean delete(Object item) – 계속 boolean delete(Object item){ if(item==null) return false; for(int loc = 0; loc<size; loc++) if(item.equals(element[loc])){ if(loc!=size-1) elements[loc] = elements[size-1]; size--; return true; } return false; }
9/25
Object next(), boolean hasNext() 사전조건: 없음 사후조건: 커서가 리스트의 맨 처음을 가리키도록 한다. void reset(){ cursor=0; } 사전조건: 없음 사후조건: 커서가 리스트에 끝을 가리 키고 있으면 false 반환하고, 그렇지 않으면 true를 반환한다. boolean hasNext(){ return (cursor<size); }
사전조건: 없음 사후조건: 현재 커서가 가리키고 있는 요소를 반환하고, 커서를 다음 요소를 가리키도록 이동한다. Object next(){ int loc = cursor; cursor++; return elements[loc]; // Listable x = // (Listable)elements[loc]; // return x.clone(); } 사용법 reset(); while(hasNext()){ next() }
reset(); for(int i=0; i<size(); i++){ next() } 10/25 10/25
SortedList ADT UnsortedList와 제공해야 되는 연산이 유사하다. 리스트가 내부적으로 정렬되어 유지되나 정렬되지 않은 상태로 유지되나 외부에서 보는 관점에서는 차이가 없다. 재사용을 고려 방법1. 직접 상속 Æ 논리적으로 SortedList가 UnsortedList의 자식 클래스는 아니다. SortedList is a UnsortedList (?) 논리적으로 상속이 적합하지 않아도 상속을 사용할 수 있지만 다음과 같은 문제가 발생할 수 있다. 예4.3) UnsortedList unsorted; SortedList sorted = new SortedList(10); unsorted = sorted; // ???
11/25 11/25
SortedList ADT – 계속 방법2. abstract 클래스 사용 List라는 클래스를 만들고, UnsortedList와 SortedList 모두 List의 자식 클래스로 만든다. 이 경우 두 클래스의 공통된 코드는 List에 한번 정의하여 사용할 수 있다. 하지만 search, insert, delete 메소드는 두 클래스의 경우 달라야 하므로 List에서 제공할 수 없다. Æ 추상 클래스 방법3. 인터페이스 사용 인터페이스는 코드의 재사용은 아니다. 사용자에게 두 클래스가 공통된 메소드를 제공한다는 것을 알려주는 역할밖에 없음 UnsortedList와 SortedList가 List 클래스 외에 다른 클래스를 상속받아야 하면 인터페이스가 유일한 대안
12/25 12/25
public abstract class List{ public final int DEF_CAPACITY=50; protected Object[] elements; protected int size = 0; protected int cursor = -1; public List(){ … } public List(int capacity){ … } private void setup(int capacity){ … } public boolean isFull(){ return (size >= element.length); } public int size(){ return size; } public boolean isEmpty(){ return (size == 0); } public abstract boolean search(Object item); public abstract boolean insert(Object item); public abstract boolean delete(Object item); public abstract Object retrieve(Object item); public void reset(){ cursor = 0; } public List(){ public boolean hasNext(){ setup(DEF_CAPACITY); return cursor<size; } } public List(int capacity){ public Object next(){ if(capacity>0) setup(capacity); int loc = cursor; else setup(DEF_CAPACITY); cursor++; } return element[loc]; private void setup(int capacity){ } elements = new Object[capacity]; } // List } 13/25 13/25
SortedList ADT public class SortedList extends List{ public SortedList(){ super(); } public SortedList(int capacity){ super(capacity); } public boolean search(Object item){ … } public boolean insert(Object item){ … } public boolean delete(Object item){ … } public Object retrieve(Object item){ … } } // SortedList
14/25 14/25
boolean insert(Object item) 사전조건: item!=null, 리스트가 full이 아님 사후조건: 리스트가 계속 정렬되어 있도록 주어진 item을 추가한다. 추가에 성공하면 true를, 못하면 false를 반환한다. 방법 1. 삽입할 위치를 찾는다. 2. 삽입할 공간을 만든다. 3. 새 요소를 리스트에 삽입한다. 이 때 맨 앞에, 중간에, 끝에 삽입되는 경우를 고려해야 한다. 맨 앞이나 중간이나 절차는 같다.
public boolean insert(Object item){ int loc = 0; boolean moreToSearch = (loc<size); if(item!=null || isFull()) return false; Comparable x = (Comparable)item; while(moreToSearch){ if(x.compareTo(elements[loc])<0) moreToSearch = false; else{ loc++; moreToSearch = (loc < size); } } // while for(int i=size; i>loc; i--) elements[i] = elements[i-1]; elements[loc] = item; size++; return true; } 15/25 15/25
boolean insert(Object item) A
B
A
B
A
B
D
C
E D
E
D
E public boolean insert(Object item){ int loc; if(item!=null || isFull()) return false; Comparable x = (Comparable)item; for(loc=0; loc<size; loc++){ if(item.compareTo(elements[loc])<0) break; for(int i=size; i>loc; i--) elements[i] = elements[i-1]; elements[loc] = item; size++; return true; }
16/25 16/25
boolean delete(Object item) 사전조건: item!=null, 사후조건: item이 리스트에 존재하면 삭제한다. 삭제에 성공하면 true를, 못하면 false를 반환한다.
A
B
C
D
A
B
D
E
E
public boolean delete(Object item){ int loc = 0, comp = 0; boolean moreToSearch = (item!=null)&&(loc<size); boolean found = false; Comparable x = (Comparable)item; while(moreToSearch && !found){ comp = x.compareTo(elements[loc]); if(comp==0) found = true; else if(comp<0) moreToSearch = false; else{ loc++; moreToSearch = (loc < size); } } // while if(found){ for(int i=loc; i<size-1; i++) elements[i] = elements[i+1]; size--; return true; } else return false; } 17/25 17/25
public boolean delete(Object item){ if(item==null) return false; int loc = 0, comp = 0; boolean found = false; Comparable x = (Comparable)item; for(loc=0; loc<size; loc++){ comp = item.compareTo(elements[loc]); if(comp==0){ found = true; break; } else if(comp<0){ found = false; break; } } // for if(found){ for(int i=loc; i<size-1; i++) elements[i] = elements[i+1]; size--; return true; } else return false; } 18/25 18/25
public boolean search(Object item){ int first = 0, mid = 0, last = size-1, comp = 0; boolean moreToSearch = true; boolean found = false; if(item!=null || isEmpty()) return false; Comparable x = (Comparable)item; while(moreToSearch && !found){ mid = (first + last) / 2; comp = x.compareTo(elements[mid]); if(comp==0) found = true; else if(comp<0){ last = mid - 1; moreToSearch = (first <= last); } else{ first = mid + 1; moreToSearch = (first <= last); } 사전조건: item!=null, } // while return found; 사후조건: item이 리스트에 있으면 true를, } 없으면 false를 반환한다.
A
B
C
D
E
F
G H
D
E
F
G H
D
E
F
G H
first=0 last=7 mid=3 A
B
C
first=0 last=2 mid=1 A
B
C
first=2 last=2 mid=2 19/25 19/25
A
B
C
D
E
G H
A
B
C
D
E
G H
A
B
C
D
E
G H
A
B
C
D
E
G H
first=0 last=6 mid=3 first=4 last=6 mid=5 first=4 last=4 mid=4
first=5 last=4 mid=?
20/25 20/25
알고리즘 비교 알고리즘의 비교는 보통 성능의 비교 Which one is faster(efficient)? 비교하기 위해서는 비교 기준이 있어야 한다. 가장 단순한 방법: 실행 시간 비교 주어진 입력에 대해 주어진 컴퓨터에서는 A 알고리즘이 B 알고리즘보다는 효율적이라는 것 밖에는 알 수 없다. 수행되는 프로그램 문장 수를 비교 프로그래밍 스타일에 의존한다. 프로그래밍 언어마다 다를 수 있다. 알고리즘의 핵심이 되는 연산의 수행 횟수 비교 검색: 비교 연산 주어진 입력에 대한 함수로 횟수를 나타냄
21/25 21/25
Big-O 알고리즘의 성능 = 핵심 연산의 수행 회수 = f(입력의 크기) Big-O 표기법을 이용하여 함수를 요약하여 표현할 수 있다. 예4.4) f(N)=N4+100N2+10N+50 Æ O(N4) N이 매우 크면 N4가 다른 것을 압도하게 된다. 입력의 크기란? 고려되는 문제의 크기 Æ 예4.5) 리스트에 있는 원소의 개수 예4.6) 리스트에 있는 요소를 파일에 기록하기 open file while(more elements in list) write the next element close file (N±한 요소를 기록하는데 소요되는 시간) + (파일 열고 닫는 시간) O(N)
22/25 22/25
Common Order of Magnitude O(1): 입력 크기에 전혀 영향을 받지 않는 경우 예4.7) 배열에 한 요소 저장하기 O(logN): 로그 시간 한번에 처리해야 하는 양이 반씩 줄어드는 경우 예4.8) 이진 검색 N logN NlogN O(N): 선형 시간 1 0 1 예4.9) 선형 검색 4 2 8 O(NlogN) O(N2) 8 3 24 N O(2 ): 지수 시간 32 5 160 O(N!): 계승 시간 256
8
2048
N2
2N
1
2
16
16
256
65536
32768
5년
16777216
don’t ask
23/25 23/25
1부터 N까지 합산하는 알고리즘 알고리즘 1. sum = 0; for(int i=1; i<=n; i++) sum = sum + count; 알고리즘 2. sum = ((n+1) * n)/2; 알고리즘 1은 O(N)이지만 알고리즘 2는 O(1)이다. 하지만 기준이 되는 연산이 다르다. 그러면 항상 알고리즘 2가 알고리즘 1보다 좋은가? N이 매우 작은 경우에는 알고리즘 1은 덧셈 연산만 사용하므로 더 좋을 수 있다. 알고리즘 이해도 측면에서 알고리즘 1이 더 좋을 수 있다. Big-O는 N이 매우 클 경우에 대한 비교 척도이다.
24/25 24/25
UnsortedList와 SortedList의 비교 연산
UnsortedList
SortedList
size, isFull, isEmpty, reset, next
O(1)
O(1)
search
O(N)
O(N) O(logN) 이진검색
insert 찾기 삽입 전체
O(1) O(1) O(1)
O(N) O(N) O(N)
delete 찾기 삭제 전체
O(N) O(1) O(N)
O(N) O(N) O(N)
25/25 25/25
©copyright 2005
자료구조 및 실습(INA240) NOTE 05
스택, 큐
한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 스택과 큐의 특성을 살펴보고, 배열을 이용한 스택과 큐의 구현방법을 학습한다.
2/20
스택 – 논리적 단계 스택의 특성 순서가 있는 동질 구조 가장 최근에 추가된 요소가 맨 위(top)에 있고, 가장 오래 전에 추가된 요소가 맨 밑(bottom)에 있다. LIFO(Last-In-First-Out) 구조: 구조 요소의 제거 또는 추가는 스택의 top에서만 이루어진다. 연산 push: 요소를 추가하는 연산 pop: 요소를 스택에서 제거하고, 맨 위 요소를 반환해주는 연산 top: 스택의 변화 없이 맨 위 요소를 반환해주는 연산
3/20
스택 – 논리적 단계 stack = new Stack()
stack.push(2)
stack.push(3)
3 2
stack.top()
stack.pop()
3
2
2
예외 상황 push: 배열을 이용하여 구현할 경우에는 더 이상 추가할 수 없는 상황이 발생할 수 있다. pop, top: 스택에 요소가 하나도 없을 수 있다.
4/20
스택 – 예외 StackOverflowException: 스택이 꽉 차있을 때 push를 시도하면 발생 public class StackOverflowException extends RuntimeException{ public StackOverflowException(){ super(“스택이 꽈 차 있는 상태에서 push를 시도하였음.”); } public StackOverflowException(String msg){ super(msg); } }
StackUnderflowException: 스택이 비어 있을 때 pop 또는 top을 시도하면 발생 public class StackUnderflowException extends RuntimeException{ public StackUnderflowException(){ super(“스택이 비어 있는 상태에서 pop/top을 시도하였음.”); } public StackUnderflowException(String msg){ super(msg); } }
5/20
스택 – 논리적 단계 범용 스택의 구성 SortedList의 경우에는 삽입할 위치를 정하기 위해 비교할 수 있어야 했지만 스택은 그렇지 않다. 스택에서는 search와 같은 연산이 필요 없다. Object 클래스만 활용하여도 가능하다. Object 클래스를 이용하여 구현할 경우에는 복사 방식으로 구현하기가 어렵다. 복사 방식으로 구현하고 싶으면 Listable 인터페이스를 사용한다.
6/20
ArrayStack ADT 배열을 이용한 스택의 구현 조작 int topindex=-1; void push(Object item); isFull()이 참이면 Object[] elements; StackOverflowException 참고. size가 필요 없음 발생 생성자 void pop(); ArrayStack() isEmpty()가 참이면 ArrayStack(int capacity); StackUnderflowException 발생 상태 Object top(); boolean isFull(); isEmpty()가 참이면 조건: topindex==elements.length-1 StackUnderflowException boolean isEmpty(); 발생 조건: topindex==-1
7/20
push public void push(Object item) throws StackOverflowException{ if(isFull()) throw new StackOverflowException(“Push attempted on a full stack.”); Listable x = (Listable)item; topindex++; elements[topindex] = x.clone(); } topindex = -1
topindex = 0
topindex = 1
topindex = 2
2
2
2
push(2)
push(3)
3
push(5)
3 2
2
3
5
5
3 2
8/20
push – 계속 topindex = 3
2 3 push(1) 5 1
topindex = 4
1 5 push(7) 3 2
2 3 5 1
topindex = 3
7
2
1 5 push(8) 3 2
pop()
실패
3 5 1
1 5 3 2
7
9/20
pop, top public void pop() throws StackUnderflowException{ if(isEmpty()) throw new StackUnderflowException(“Pop attempted on an empty stack”); elements[topindex] = null; // 불필요 topindex--; } public Object top() throws StackUnderflowException{ if(isEmpty()) throw new StackUnderflowException(“Top attempted on an empty stack”); Listable x = (Listable)elements[topindex]; return x.clone(); }
10/20 10/20
ArrayList를 이용한 Stack의 구현 일반 배열과 달리 public class ArrayListStack{ 용량이 고정되어 있지 private ArrayList stack; 않다. 따라서 push는 public ArrayListStack(){ stack = new ArrayList(); } 항상 성공하며, public void push(Object item){ stack.add(item); } isFull이 참이 되는 public void pop() throws StackUnderflowException{ 경우가 없다. if(isEmpty()) add는 항상 리스트 throw new StackUnderflowException(“…”); 끝에 추가한다. 따라서 stack.remove(stack.size()-1); 스택을 ArrayList로 } 구현하여도 비용 측면 public Object top() throws StackUnderflowException{ 에서 문제가 없다. if(isEmpty()) throw new StackUnderflowException(“…”); by clone 방식으로 return stack.get(stack.size()-1); 변경할 경우에는 } 다음과 같이 한다. public boolean isEmpty() { return (stack.size() == 0); } public void push(Object item){ public boolean isFull() { return false; } Listable x = (Listable)item; } stack.add(x.clone()); } 11/20 11/20
큐 – 논리적 단계 큐(queue)의 특성 순서가 있는 동질 구조 가장 최근에 추가된 요소가 맨 뒤(rear)에 있고, 가장 오래 전에 추가된 요소가 맨 앞(front)에 있다. FIFO(First-In-First-Out) 구조: 구조 요소의 추가는 큐의 끝에 이루어지고, 요소의 제거는 앞에서 이루어진다. 연산 enqueue: 요소를 큐의 끝에 추가하는 연산 dequeue: 큐의 맨 앞에 있는 요소를 제거하고, 그 요소를 반환해주는 연산
12/20 12/20
큐 – 논리적 단계 queue = new Queue()
queue.enq(2)
2
queue.enq(3)
3
queue.enq(1)
2
1
3
queue.deq()
2
1
3
예외 상황 enqueue: 배열을 이용하여 구현할 경우에는 더 이상 추가할 수 없는 상황이 발생할 수 있다. dequeue: 큐에 요소가 하나도 없을 수 있다.
13/20 13/20
고정된 Front 설계 방법 front=0, rear=-1 front=0, rear=0
enq(10);
10
enq(20);
10
20
enq(30);
10
20
30
20
30
deq(); 20
30
front=0, rear=1 front=0, rear=2
front=0, rear=1
배열의 첫 번째 슬롯을 항상 front로 고정해서 사용하는 방법 실제 front 멤버 변수가 필요 없음 enq는 항상 size-1 슬롯에 추가하면 된다. rear 멤버 변수도 필요 없음 front, rear, size 중 size 하나만 사용하여 구현 가능 deq의 경우에는 최악의 경우 n-1개의 요소를 하나씩 왼쪽으로 이동해야 한다. 14/20 14/20
유동 Front 설계 방법 enq(10);
10
deq(); enq(20);
20
enq(30);
20
30
enq(40);
20
30
40
enq(50);
20
30
40
20
30
40
enq(60);
60
front=0, rear=-1 front=0, rear=0 front=1, rear=0 front=1, rear=1 front=1, rear=2 front=1, rear=3 50 front=1, rear=4 50 front=1, rear=0
front가 고정되어 있지 않다. enq, deq 모두 O(1)이다. enq: rear 증가 deq: front 증가 추가적으로 front, rear 멤버 변수를 유지해야 하며, front와 rear 값을 통해 큐에 있는 요소의 개수를 알 수 없으면 추가적으로 size 멤버변수가 필요하다. 예에서 알 수 있듯이 full 상태와 empty 상태에서 front와 rear 값의 차이가 동일함 15/20 15/20
ArrayQueue ADT 배열을 이용한 유동 front 방식의 큐 구현 int front=0; int rear=-1; int size=0; Object[] elements; 생성자 ArrayQueue() ArrayQueue(int capacity);
상태 boolean isFull(); 조건: size==elements.length boolean isEmpty(); 조건: size==0 조작 void enq(Object item); isFull()이 참이면 QueueOverflowException 발생
Object deq(); isEmpty()가 참이면 QueueUnderflowException 발생
16/20 16/20
Enqueue public void enq(Object item) throws QueueOverflowException{ if(isFull()) throw new QueueOverflowException(“Enqueue attempted on a full queue.”); Listable x = (Listable)item; rear = (rear + 1) % elements.length; elements[rear] = x.clone(); size++; } 10 deq();
20
30
40
20
30
40
30
40
30
40
30
40
deq(); enq(55);
55
enq(70);
55
70
front=0, rear=4 front=1, 50 rear=4
50
front=2, rear=4 front=2, 50 rear=0 50
50 front=2, rear=1
30 deq();
40 40
front=4, rear=3 front=4, 55 rear=4
deq(); enq(55); enq(70);
front=2, rear=3 front=3, rear=3
65
55 front=4, rear=0 17/20 17/20
Dequeue public Object deq() throws QueueUnderflowException{ if(isEmpty()) throw new QueueUnderflowException(“Dequeue attempted on an empty queue”); Object tmp = element[front]; elements[front] = null; // 불필요 front = (front + 1) % elements.length; size--; return tmp; // tmp.clone() 불필요 } int loc = front; front = (front + 1) % elements.length; size--; return elements[loc];
18/20 18/20
효율적인 유동 front 구현 front와 rear 정보만을 이용하여 현재 큐의 상태(full 또는 empty)를 판별할 수 있는가? 현재 구현의 경우에는 두 조건이 같다. 두 조건이 같으므로 이 조건을 구분하는 boolean flag을 사용한다. size를 사용하는 것과 차이가 없음 public void enq(Object item) throws QueueOverflowException{ if(isFull()) throw new QueueOverflowException(“…”); Listable x = (Listable)item; rear = (rear + 1) % elements.length; elements[rear] = x.clone(); isFull = ((rear + 1) % elements.length)==front)? true : false; }
배열의 한 공간을 사용하지 않으면 판별할 수 있다. (How?)
19/20 19/20
효율적인 유동 front 구현 – 계속 이 방법은 size 정보를 유지하지 않아도 된다. 하지만 항상 배열 한 슬롯이 10 사용되지 않는다. 공간 측면에서는 기존 size를 10 20 30 40 50 사용하는 것과 차이는 없다. 시간 측면에서는 size를 deq(); 20 30 40 50 유지하는 비용이 없으므로 효율적이다. deq();
30
40
50
enq(60);
30
40
50
front=5, rear=5 front=5, rear=0 front=5, rear=4 front=0, rear=4 front=1, rear=4 60
front=1, rear=5
20/20 20/20
©copyright 2005
자료구조 및 실습(INA240) NOTE 06
연결구조
한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 연결구조 연결구조를 이용한 스택의 구현 연결구조를 이용한 큐의 구현 연결구조를 이용한 순환 큐의 구현 연결구조를 이용한 정렬 리스트의 구현 연결구조를 이용한 비정렬 리스트 구현
2/37
연결구조란 연결구조란 연결구조 고정된 공간을 미리 확보하여 사용하지 않고, 필요할 때마다 동적으로 확보하여 사용하는 구조를 구조 말한다. 배열을 이용한 구현 방식에서는 요소를 저장할 공간을 미리 확보하며, 이 공간이 고정되어 있다. 따라서 공간이 낭비되거나 부족할 수 있다. ArrayList처럼 부족하면 보다 큰 공간을 확보하여 새 공간에 기존 요소를 복사하여 사용하는 방식을 취할 수 있다. 이 방식은 공간이 부족할 때 많은 비용이 소요된다는 문제점이 있다. 연결구조는 하나의 요소를 저장할 때마다 공간을 확보한다. 이렇게 확보한 공간을 연결하지 않으면 저장한 요소들을 접근하기 어렵기 때문에 배열과 달리 하나의 요소를 저장할 때마다 추가 공간이 필요하다. 이것이 연결구조의 한 가지 단점이다. 필요 info
info
info
info
info
info
info
배열방식
info
연결 구조 방식 3/37
연결구조 vs. 배열을 이용한 구현 방식
공간부족/낭비 문제 한 요소가 차지하는 공간 공간 확보 비용
각 요소 접근 방식
중간에 요소 삽입
연결구조
배열을 이용한 구현 방식
없음
있음
요소+연결
요소
새 요소를 삽입할 때마다 확보해야 함 순차 정렬 구조의 경우에도 이진 검색을 할 수 없음 연결만 변경 (상수 시간)
자료구조를 처음 생성할 때 한번 확보 임의 정렬 구조의 경우에는 이진 검색 가능 정렬 구조일 경우에는 후속 요소들을 모두 이동해야 함.
4/37
Stack과 Queue의 Class Hierarchy Stack
ArrayStack
LinkedStack
use inner class
StackNode
LinkedQueue
use inner class
QueueNode
Queue
ArrayQueue
5/37
배열로 스택의 구현 배열을 이용한 리스트, 스택, 큐의 문제점 배열의 크기가 생성할 때 고정됨 Æ 공간의 낭비, 공간의 부족 배열을 이용한 스택의 물리적 뷰 vs. 논리적 뷰 ArrayStack stack = new ArrayStack(4); stack
topIndex element
-1 null
null
null
null
null
null
null
stack.push(new Integer(10)) stack
topIndex
0
element
10
10
6/37
연결구조로 스택의 구현 연결구조 info
link
info
A
link
info
B
null
private class StackNode{ public Object info; public StackNode link; }
C
연결구조로 스택의 구현 LinkedStack stack = new LinkedStack(); stack
top
null
stack.push(new Integer(20)); stack
top
stack.push(new Integer(10)); stack
top
info
null
10
info
info
20
10
null
7/37
LinkedStack ADT 연결구조를 이용한 스택의 구현 StackNode top = null; 연결 구조이므로 배열과 달리 색인이 아닌 참조 타입으로 스택 top을 유지함
생성자 LinkedStack() 상태 boolean isFull(); 조건: 항상 거짓 boolean isEmpty(); 조건: top==null
조작 void push(Object item); StackOverflowException이 발생하지 않음
void pop(); isEmpty()가 참이면 StackUnderflowException 발생
Object top(); isEmpty()가 참이면 StackUnderflowException 발생 stack
top
null
8/37
Inner Class public class LinkedStack implements Stack{ private class StackNode{ public Object item; public StackNode link; } private StackNode top = null; public StackNode() {} … } private 내부 클래스는 그것의 외부 클래스를 제외하고는 접근할 수 없으므로 내부 클래스의 멤버 변수를 public으로 선언하여도 안전하다.
클래스 내부에 정의한 클래스를 내부 클래스(inner class)라 한다.
class public class private class public class
패키지
9/37
Inner Class 내부 클래스를 사용하는 목적 내부 클래스는 그것을 생성한 외부 클래스 객체의 구현 (멤버 변수, 메소드)을 접근 권한과 무관하게 모두 접근할 수 있다. 같은 패키지에 있는 다른 클래스로부터 숨길 수 있다. (name control 측면) 연결 구조에서는 이 측면 때문에 사용 private 내부 클래스를 사용하여 사건 기반 프로그램을 작성할 때 매우 유용하다. top level class
당연한 특성 inner class
외부 클래스 객체의 구현을 접근할 수 있다. static inner class인 경우에는 가능하지 않다.
10/37 10/37
public class Test{ public static void main(String[] args){ Outer obj = new Outer(2, 3); obj.g(3); obj.print(); } } 결과: oVal = 6, iVal = 9
public class Outer{ private int oVal; private Inner X; private class Inner{ private int iVal; public Inner(int n){ iVal = n; } obj public void f(int n){ iVal = oVal + n; // Outer.this.oVal+n oVal X } 6 2 내부 클래스는 외부 클래스의 } // class Inner private 멤버를 접근할 수 있다. public Outer(int n1, int n2){ oVal = n1; X = new Inner(n2); } 외부 클래스는 내부 클래스의 public void g(int n){ private 멤버를 접근할 수 있다. 3 9 oVal = X.iVal + n; X.f(n); iVal Outer } public void print(){ System.out.println("oVal = "+oVal+", " 내부 클래스의 객체가 생성될 때 +"iVal = "+X.iVal); 이 객체의 외부 객체에 대한 참조는 } 현재 객체의 this값으로 설정된다. } // class Outer 11/37 11/37
push push 알고리즘 단계 1. 새 요소를 위한 노드 생성 단계 2. 이 요소의 info 값에 item 할당 단계 3. 이 요소의 link가 기존 top을 가리키도록 함 단계 4. top이 이 요소를 가리키도록 함 stack
public void push(Object item){ StackNode newNode = new StackNode(); newNode.info = item; newNode.link = top; top = newNode; }
top
4 1. 생성 info
info
null
3 2
10
20
12/37 12/37
pop pop 알고리즘 단계 1. 스택이 비어 있는지 확인함 단계 2. top이 top.link을 가리키도록 함
stack
top
public void pop() throws StackUnderflowException{ if(isEmpty()) throw new StackUnderflowException(“…”); top = top.link; }
1 info
info
20
10
null
garbage
13/37 13/37
top public Object top() throws StackUnderflowException{ if(isEmpty()) throw new StackUnderflowException(“…”); return top.info; }
stack
top
info
info
null
10 20
14/37 14/37
ArrayStack VS. LinkedStack ArrayStack
LinkedStack
push
O(1)
O(1)
pop
O(1)
O(1)
constructor
O(N)
O(1)
N+1 참조, 1 색인
2N+1 참조 (no waste)
space
Which one is better? 상황에 따라 다르다. 공간 활용 측면에서 LinkedStack이 우수 스택의 크기가 매우 가변적인 경우 LinkedStack이 우수 ArrayStack은 공간 낭비 가능 push가 빈번하게 일어나는 경우: ArrayStack 우수 LinkedStack은 매번 새 공간을 확보해야함 15/37 15/37
배열로 큐의 구현 ArrayQueue queue = new ArrayQueue(4); queue
front
0
rear null
element
-1
null
size null
0 null
queue.enq(new Character(‘A’)) queue
front
0
rear
0
null
element
배열을 이용한 큐의 물리적 뷰 vs. 논리적 뷰
front
rear size
null
1
A
null
A
queue.enq(new Character(‘B’)) queue
front
0
rear
1
size null
element
A
front
rear 2 null
B
A
B
16/37 16/37
연결구조로 큐의 구현 연결구조로 큐의 구현 LinkedQueue queue = new LinkedQueue(); queue
front
null
rear null
size
0
size
1
private class QueueNode{ public Object info; public QueueNode link; }
queue.enq(new Character(‘A’)); queue
front
info
rear
null
A
queue.enq(new Character(‘B’)); queue
front
size
rear
info
info
A
B
2
null
17/37 17/37
LinkedQueue ADT 연결 구조를 이용한 큐의 구현 QueueNode front = null; QueueNode rear = null; int size = 0; 생성자 LinkedQueue() 상태 boolean isFull(); 조건: 항상 거짓 boolean isEmpty(); 조건: front==null
조작 void enq(Object item); QueueOverflowExceptio은 발생하지 않음
Object deq(); isEmpty()가 참이면 QueueUnderflowException 발생
18/37 18/37
enqueue enqueue 알고리즘 단계 1. 새 요소를 위한 노드 생성 단계 2. 이 요소의 info 값에 item 할당 단계 3. 이 요소를 rear에 추가 단계 4. rear가 이 요소를 가리키도록 변경함 public void enq(Object item) { QueueNode newNode = new QueueNode(); newNode.info = item; newNode.link = null; // 큐가 비어 있는 경우 if(rear == null) front = newNode; // 큐에 요소가 있는 경우 else rear.link = newNode; rear = newNode; size++; }
queue
front
size
rear
3 info
2
info
10
2 1. 생성 null
20
19/37 19/37
dequeue dequeue 알고리즘 단계 1. queue가 비어있는지 검토 단계 2. front.item을 임시 보관 단계 3. front가 front.link를 가리키도록 변경함 단계 4. 큐에 더 이상 요소가 없으면 rear를 null로 설정함 단계 5. 임시 보관한 item을 반환 queue
front
rear
size
1
info
info
null
10
20
2
tmp
1
public Object deq() throws QueueUnderflowException{ if(isEmpty()) throw new QueueUnderflowException(“…”); Object tmp = front.info; front = front.link; size--; if(isEmpty()) rear = null; return tmp; } 20/37 20/37
Circular Linked Queue queue
rear
info
info
info
10
20
30
순환 연결 구조를 이용하면 하나의 참조만을 이용하여 큐를 구현할 수 있다. front만 유지하면 dequeue는 O(1)이지만 enqueue는 O(n)이 된다. rear만 유지하면 큐 맨 뒤는 rear로 큐 맨 앞은 rear.link로 바로 접근할 수 있다.
21/37 21/37
Circular Linked Queue: enq public void enq(Object item) { QueueNode newNode = new QueueNode(); newNode.info = item; newNode.link = null; if(rear == null){ // list가 empty인 경우 rear = newNode; rear.link = newNode; } else{ newNode.link = rear.link; rear.link = newNode; rear = newNode; } size++; }
queue
3
rear
info
info
10
20
2
info 30
1
22/37 22/37
ArrayQueue VS. LinkedQueue ArrayQueue
LinkedQueue
enqueue
O(1)
O(1)
dequeue
O(1)
O(1)
constructor
O(N)
O(1)
N+1 참조, 3 색인
2N+2 참조(no waste)
space
Which one is better? 상황에 따라 다르다. 공간 활용 측면에서 LinkedStack이 우수: 큐에 있는 요소가 배열 용량의 반 이하일 경우
23/37 23/37
List Class Hierarchy List
ArrayList
UnsortedArrayList
import java.util.Iterator; public interface List extends Iterable{ boolean isEmpty(); boolean isFull(); void clear(); int size(); boolean search(Object item); void insert(Object item); boolean delete(Object item); Object retrieve(Object item); Iterator iterator(); } // List
LinkedList
SortedArrayList
use inner class
UnsortedLinkedList
ListNode
SortedLinkedList
24/37 24/37
Import java.util.Iterator; public abstract class LinkedList implements List{ protected class ListNode{ public Object info; public ListNode next; } // ListNode protected class ListIterator implements Iterator{ … ListNode와 ListIterator 내부 클래스가 } // ListIterator protected인 이유는 자식 클래스인 protected ListNode list = null; UnsortedLinkedList와 protected int size = 0; SortedLinkedList에서 활용하기 위함이다. public LinkedList(){} public boolean isFull(){ return false; } public boolean isEmpty(){ return (list == null); } void clear(){ … } public int size(){ return size; } public Iterator iterator() { return new ListIterator(list); } public abstract boolean search(Object item); public abstract void insert(Object item); public abstract boolean delete(Object item); public abstract Object retrieve(Object item); } // LinkedList 25/37 25/37
반복자 클래스 protected class ListIterator implements Iterator{ public ListNode cursor; public int traverseCount = 0; public ListIterator(ListNode node){ cursor = node; } public Object next(){ 반드시 반복자 클래스를 내부 클래스로 Object tmp = cursor.info; 구현할 필요는 없다. cursor = cursor.next; traverseCount++; return tmp; } traverseCount 변수를 사용하지 않고 public boolean hasNext(){ cursor만을 이용하여 구현할 수도 있다. return (traverseCount<size); 하지만 CircularLinkedList까지 고려하여 // return (cursor!=null) traverseCount 변수를 활용하고 있다. } public void remove(){ throw new UnsupportedOperationException(); } } // ListIterator 26/37 26/37
UnsortedLinkedList, SortedLinkedList public class UnsortedLinkedList extends LinkedList{ public UnsortedLinkedList() { super(); } public boolean search(Object item) throws ListUnderflowException{ … } public void insert(Object item){ … } public boolean delete(Object item) throws ListUnderflowException{ … } public Object retrieve(Object item) throws ListUnderflowException{ …} } // UnsortedLinkedList public class SortedLinkedList extends LinkedList{ public SortedLinkedList() { super(); } public boolean search(Object item) throws ListUnderflowException{ … } public void insert(Object item){ … } public boolean delete(Object item) throws ListUnderflowException{ … } public Object retrieve(Object item) throws ListUnderflowException{ …} } // UnsortedLinkedList
이진검색을 할 수 없다. 정렬되어 있을 경우에는 검색을 중간에 중단할 수 있다. 27/37 27/37
UnsortedLinkedList: search public boolean search(Object item) throws ListUnderflowException{ if(isEmpty()) throw new ListUnderflowException(“…”); if(item==null) throw new NullPointerException(“…”); // if(item==null) return false; ListNode loc = list; boolean moreToSearch = true; do{ if(item.equals(loc.info)) return true; else{ loc = loc.next; moreToSearch = (loc != null); } }while(moreToSearch); return false; } // UnsortedList: search loc info
info
info
info
10
50
30
20
28/37 28/37
UnsortedLinkedList: search
public boolean search(Object item) throws ListUnderflowException{ if(isEmpty()) throw new ListUnderflowException(“…”); if(item==null) throw new NullPointerException(“…”); ListNode loc = list; for(int i=0; i<size; i++){ if(item.equals(loc.info)) return true; else loc = loc.next; } // for return false; } // UnsortedList: search
29/37 29/37
SortedLinkedList: search public boolean search(Object item) throws ListUnderflowException{ if(isEmpty()) throw new ListUnderflowException(“…”); if(item==null) throw new NullPointerException(“…”); Comparable x = (Comparable)item; ListNode loc = list; boolean moreToSearch = true; do{ int compResult = x.compareTo(loc.info); if(compResult==0) return true; else if(compResult<0) return false; else{ loc = loc.next; moreToSearch = (loc != null); } }while(moreToSearch); return false; } // SortedLinkedList: search (중간에 중단 가능) list info
info
info
info
10
20
30
50
30/37 30/37
SortedLinkedList: search public boolean search(Object item) throws ListUnderflowException{ if(item==null) throw new NullPointerException(“…”); if(isEmpty()) throw new ListUnderflowException(“…”); Comparable x = (Comparable)item; ListNode loc = list; for(int i=0; i<size; i++){ int compResult = x.compareTo(loc.info); if(compResult==0) return true; else if(compResult<0) return false; else loc = loc.next; } // for return false; } // SortedLinkedList: search (중간에 중단 가능)
31/37 31/37
public boolean delete(Object item) throws ListUnderflowException{ if(isEmpty()) throw new ListUnderflowException(“…”); if(item==null) throw new NullPointerException(“…”); ListNode loc = list; ListNode prevLoc = null; for(int i=0; i<size; i++){ if(x.equals(loc.info)){ // 삭제할 요소가 있는 경우 if(prevLoc==null) list = list.next; // 리스트의 처음 else prevLoc.next = loc.next; size--; return true; } else{ // 검색을 계속해야 하는 경우 prevLoc = loc; loc = loc.next; } } // for return false; } // UnsortedLinkedList: delete list
prevLoc
loc
info
info
info
info
10
50
30
10 32/37 32/37
SortedLinkedList: delete public boolean delete(Object item) throws ListUnderflowException{ if(item==null) throw new NullPointerException(“…”); if(isEmpty()) throw new ListUnderflowException(“…”); Comparable x = (Comparable)item; ListNode loc = list; ListNode prevLoc = null; for(int i=0; i<size; i++){ int compResult = x.compareTo(loc.info); if(compResult==0){ // 삭제할 요소가 있는 경우 if(prevLoc==null) list = list.next; // 리스트의 처음 else prevLoc.next = loc.next; size--; return true; } // 삭제할 요소가 없는 경우 else if(compResult<0) return false; else{ // 검색을 계속해야 하는 경우 prevLoc = loc; loc = loc.next; } } // for return false; } // SortedLinkedList: delete
33/37 33/37
UnsortedLinkedList: insert 배열 구현의 경우에는 맨 끝에 연결 구조 구현의 경우에는 맨 처음에
2
public void insert(Object item){ ListNode newNode = new ListNode(); newNode.info = item; newNode.next = list; list = newNode; size++; } // UnsortedLinkedList: insert
list
info 25
1 info
info
info
info
10
50
30
10
34/37 34/37
public void insert(Object item){ if(item==null) throw new NullPointerException(“…”); Comparable x = (Comparable)item; ListNode newNode = new ListNode(); newNode.info = item; ListNode loc = list; ListNode prevLoc = null; for(int i=0; i<size; i++){ // 삽입할 위치 결정 if(x.compareTo(loc.info)<0) break; else{ prevLoc = loc; loc = loc.next; } } // for if(prevLoc==null){ // 리스트의 맨 처음 newNode.next = list; list = newNode; 2 } else{ // 리스트 중간 또는 끝 newNode.next = loc; info prevLoc.next = newNode; } 20 size++; } // SortedLinkedList: insert
list
1 info 10
info 25
1 info 30
35/37 35/37
반복자의 사용 void print(SortedLinkedList l){ Iterator i = l.iterator(); while(i.hasNext()){ System.out.print(i.next()+”,”); } System.out.println(); }
36/37 36/37
List의 비교 Unsorted, Array
Unsorted, Linked
Sorted, Array
Sorted, Linked
search
O(N)
O(N)
O(logN)
O(N)
retrieve
O(N)
O(N)
O(logN)
O(N)
insert
O(1)
O(1)
O(logN), O(N) 전체: O(N)
O(N), O(1) 전체: O(N)
delete
O(N), O(1) 전체: O(N)
O(N), O(1) 전체: O(N)
O(logN), O(N) 전체: O(N)
O(N), O(1) 전체: O(N)
space
N+1 참조, 2 정수
2N+2 참조, 1 정수
N+1 참조, 2 정수
2N+2 참조, 1 정수
37/37 37/37
©copyright 2005
자료구조 및 실습(INA240) NOTE 07
연결 구조 - 확장
한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 순환 연결 리스트 이중 연결 리스트 배열을 이용한 연결구조 방식의 리스트 구현
2/34
순환 연결 리스트 연결 리스트에서 마지막 노드의 링크를 첫 노드를 가리키도록 변형한 리스트 list
info
info
info
10
20
30
장점. 어떤 노드에서 출발해도 전체 리스트를 방문할 수 있다. 기존 연산들은 항상 마지막 노드가 첫 노드를 가리키도록 수정해야 한다. 수정시 가장 큰 문제점 첫 노드의 삭제 또는 리스트 맨 앞에 노드의 삽입: 마지막 노드까지 이동하여야 한다.
3/34
순환 연결 리스트 – 계속 해결책: 해결책 첫 노드 대신에 마지막 노드를 가리키는 포인터 유지 info
info
info
10
20
30
list
첫 노드와 마지막 노드를 접근하기가 모두 용이함 마지막 노드의 내용: list.info 첫 노드의 내용: list.next.info info
list
30
4/34
List Class Hierarchy
5/34
CircularSortedLinkedList public class CircularSortedLinkedList extends LinkedList{ public CircularSortedLinkedList() { super(); } public boolean search(Object item) throws ListUnderflowException{ … } public void insert(Object item){ … } public boolean delete(Object item) throws ListUnderflowException{ … } public Object retrieve(Object item) throws ListUnderflowException{ … } public Iterator iterator() { return (list==null) ? new ListIterator(null) : new ListIterator(list.next); } }
6/34
CircularSortedLinkedList의 search public boolean search(Object item) throws ListUnderflowException{ if(isEmpty()) throw new ListUnderflowException(“…”); Comparable x = (Comparable)item; ListNode loc = list.next; for(int i=0; i<size; i++){ int compResult = x.compareTo(loc.info); if(compResult==0) return true; else if(compResult<0) return false; else loc = loc.next; } return false; } // CircularSortedList::search loc
list
info
info
info
info
10
20
30
40
7/34
CircularSortedLinkedList의 delete prevLoc
loc
list
info
info
info
info
10
20
30
40
prevLoc
loc
list
info
info
info
info
10
20
30
40
일반적인 경우 prevLoc.next = loc.next;
첫 노드 list.next = list.next.next; 또는 list.next = loc.next;
8/34
CircularSortedLinkedList의 delete loc prevLoc
list
info
info
info
info
10
20
30
40 마지막 노드 prevLoc.next = loc.next; list = prevLoc;
loc prevLoc
list
info
유일 노드 list = null;
10
9/34
public boolean delete(Object item) throws ListUnderflowException{ if(isEmpty()) throw new ListUnderflowException(“…”); Comparable x = (Comparable)item; ListNode loc = list.next; ListNode prevLoc = null; for(int i=0;i<size;i++){ int compResult = x.compareTo(loc.info); if(compResult==0){ // 삭제할 노드가 존재하는 경우 if(prevLoc==null){ // 첫 노드를 삭제하는 경우 if(loc == loc.next) list = null; // 첫 노드가 유일 노드인 경우 else list.next = loc.next; if(loc == loc.next) 대신에 } if(size==1)를 사용할 수 있음 else{ prevLoc.next = loc.next; if(loc==list) list = prevLoc; // 마지막 노드를 삭제하는 경우 } size--; return true; } else if(compResult<0) return false; else{ prevLoc = loc; loc = loc.next; } } // for return false; } // CircularSortedList::delete 10/34 10/34
첫 노드와 마지막 노드를 모두 접근하기가 용이하다는 순환 연결 구조의 특성을 이용한 delete 메소드
public boolean delete(Object item) throws ListUnderflowException{ if(isEmpty()) throw new ListUnderflowException(“…”); Comparable x = (Comparable)item; if(x.compareTo(list.next.info)<0||x.compareTo(list.info)>0){ return false; } // 나머지 부분은 동일 } // CircularSortedList::delete
11/34 11/34
CircularSortedLinkedList의 insert loc prevLoc
list
info
info
info
10
20
40
빈 리스트 list = newNode; list.next = newNode; loc prevLoc
list
info 일반적인 경우 newNode.next = loc; prevLoc.next = newNode;
30
info 10
12/34 12/34
CircularSortedLinkedList의 insert prevLoc
loc
list
info
info
info
info
10
20
30
40
첫 노드 newNode.next = loc; list.next = newNode;
prevLoc loc
list
info
info
info
info
10
20
30
40
마지막 노드 newNode.next = loc; prevLoc.next = newNode; list = newNode;
13/34 13/34
public void insert(Object item){ Comparable x = (Comparable)item; ListNode newNode = new ListNode(); newNode.info = item; newNode.next = null; ListNode loc = (list==null)? null: list.next; ListNode prevLoc = null; for(int i=0;i<size;i++){ // 삽입할 위치 찾기 if(x.compareTo(loc.info)<0) break; else{ prevLoc = loc; loc = loc.next; } } // for if(prevLoc==null){ // 리스트 맨 앞에 삽입되는 경우 // 리스트가 비어있는 경우 if(list==null){ list = newNode; newNode.next = newNode; } else{ newNode.next = loc; list.next = newNode; } } else{ newNode.next = loc; prevLoc.next = newNode; // 리스트의 맨 마지막에 삽입되는 경우 if(prevLoc == list) list = newNode; } size++; } // CircularSortedList::insert 14/34 14/34
첫 노드와 마지막 노드를 public void insert(Object item){ 모두 접근하기가 용이하다는 Comparable x = (Comparable)item; 순환 연결 구조의 특성을 ListNode newNode = new ListNode(); 이용한 insert 메소드 newNode.info = item; newNode.next = null; size++; if(list==null){ list = newNode; newNode.next = newNode; return; } // 리스트가 비어 있는 경우 if(x.compareTo(list.next.info)<0||x.compareTo(list.info)>0){ newNode.next = list.next; list.next = newNode; if(x.compareTo(list.info)>0) list = newNode; // 맨 끝에 삽입되는 경우 return; } // 삽입하고자 하는 노드가 첫 노드보다 작거나 마지막 노드보다 큰 경우 // 첫 노드와 비교할 필요 없음 ListNode loc = list.next.next; ListNode prevLoc = list.next; for(int i=0;i<size;i++){ if(x.compareTo(loc.info)<0){ newNode.next = loc; prevLoc.next = newNode; return; } else{ prevLoc = loc; loc = loc.next; } } // for } // CircularSortedList::insert 15/34 15/34
CircularSortedLinkedList의 장단점 연산 측면에서는 SortedLinkedList에 비해 향상된 것이 없다. 그러나 CircularSortedLinkedList는 일반 정렬 연결리스트와 달리 첫 노드와 마지막 노드에 대한 접근이 용이하다. 따라서 다음과 같은 것은 일반 정렬 연결리스트보다 우수하다. 가장 큰 값의 추출 리스트에 저장된 값들 사이에 있는 값인지 비교하는 것 정렬된 데이터를 연속해서 삽입하는 경우 일반 정렬 연결리스트에 마지막 노드를 가리키는 포인터를 하나 더 유지하는 것은? good alternative
16/34 16/34
이중 연결 리스트 이중 연결 리스트(doubly linked list)란 첫 노드와 마지막 리스트 노드를 제외하고는 모두 후속 요소뿐만 아니라 선행 요소를 가리키는 포인터를 추가로 가지고 있는 리스트 list
info
info
info
10
20
30
어떤 노드에서 출발해도 전체 리스트를 방문할 수 있다. 역순으로 노드들을 방문할 수 있다. 노드의 구성: back, info, next protected class DLListNode{ public Object info; 3N+1 public DLListNode back; public DLListNode next; }
17/34 17/34
List Class Hierarchy
18/34 18/34
public abstract class DoubleLinkedList implements List{ protected class ListNode{ public Object info; public ListNode back; public ListNode next; } protected class ListIterator{ â&#x20AC;Ś } protected ListNode list = null; protected int size = 0; public DoubleLinkedList() {} public abstract boolean search(Object item) throws ListUnderflowException; public abstract void insert(Object item); public abstract boolean delete(Object item) throws ListUnderflowException; public abstract Object retrieve(Object item) throws ListUnderflowException; public boolean isFull(){ return false; } public boolean isEmpty(){ return (list == null); } public void clear{ list = null; } public int size(){ return size; } public Iterator iterator() { return new ListIterator(list); } } // DoubleLinkedList class 19/34 19/34
protected class ListIterator{ public ListNode cursor; public int traverseCount = 0; public ListIterator(ListNode node){ cursor = node; } // ListIterator(ListNode) public boolean hasBack(){ return (traverseCount>0); } public boolean hasNext(){ return (traverseCount<size); } public Object back(){ Object tmp = cursor.info; cursor = cursor.back; traverseCount--; return tmp; } // back public Object next(){ Object tmp = cursor.info; cursor = cursor.next; traverseCount++; return tmp; } // next public void remove(){ throw new UnsupportedOperationException(); } // remove } 20/34 20/34
SortedDoubleLinkedList의 Insert prevLoc
list
2
3
1
loc
prevLoc
loc
info
info
info
10
20
3
4
30
1
1 2
2
info
info
info
5
25
50
newNode.next = loc; loc.back = newNode; list = newNode;
newNode.back = prevLoc; newNode.next = loc; prevLoc.next = newNode; loc.back = newNode;
newNode.back = prevLoc; prevLoc.next = newNode;
21/34 21/34
public void insert(Object item){ Comparable x = (Comparable)item; ListNode newNode = new ListNode(); newNode.info = item; newNode.back = null; newNode.next = null; ListNode loc = list; ListNode prevLoc = null; for(int i=0;i<size;i++){ // 삽입할 위치를 찾는다. if(x.compareTo(loc.info)<0) break; else{ prevLoc = loc; loc = loc.next; } } // for if(prevLoc==null){ // 리스트 맨 앞에 삽입되는 경우 list = newNode; newNode.next = loc; if(list!=null) loc.back = newNode; // 빈 리스트가 아닌 경우 } else{ newNode.back = prevLoc; newNode.next = loc; prevLoc.next = newNode; if(loc!=null) loc.back = newNode; // 맨 마지막에 삽입되는 경우가 아니면 } size++; } // SortedDoubleLinkedList::insert 22/34 22/34
SortedDoubleLinkedList의 delete prevLoc
loc
1 info
list
info
info
2 10
prevLoc
20
30
info
info
info
10
20
30
일반적인 경우 prevLoc.next = loc.next; loc.next.back = prevLoc;
loc
1 list
리스트의 첫 노드를 삭제하는 경우 loc.next.back = null; list = loc.next;
23/34 23/34
SortedDoubleLinkedList의 delete
list
prevLoc
list
prevLoc
loc
info
info
info
10
20
30
리스트의 마지막 노드를 삭제하는 경우 prevLoc.next = null;
loc
info
리스트에 있는 유일 노드를 삭제하는 경우 list = null;
10
24/34 24/34
public boolean delete(Object item) throws ListUnderflowException{ if(isEmpty()) throw new ListUnderflowException("…"); Comparable x = (Comparable)item; ListNode loc = list; List prevLoc = null; for(int i=0;i<size;i++){ int compResult = x.compareTo(loc.info); if(compResult==0){ // 삭제할 노드가 존재하는 경우 if(prevLoc==null){ // 첫 노드를 삭제하는 경우 if(loc.next==null) list = null; // 삭제할 노드가 유일 노드인 경우 else{ loc.next.back = null; list = loc.next; } } else{ prevLoc.next = loc.next; if(loc.next != null) loc.next.back = prevLoc; // 마지막 노드가 아닌 경우 } return true; } else if(compResult<0) return false; else{ prevLoc = loc; loc = loc.next; } } // for return false; } 25/34 25/34
배열을 이용한 연결구조의 구현
list
색인
info
next
0
David
2
1
null
5
2
John
4
3
Peter
6
4
Mary
3
5
null
END
6
Robert
END
0
free
1
class ArrayLinkedList{ public static final int DEF_MAX_CAPACITY = 50; public static final int END = -1; private class ListNode{ public Object info; public int next; } private int list = END; private ListNode[] nodes; private int size = 0; private int free = 0; … }
size
5
배열을 이용한 연결구조에서 연결은 배열의 색인정보가 된다. 따라서 null 대신에 -1을 이용하여 리스트의 끝을 나타낸다.
26/34 26/34
배열을 이용한 연결구조의 구현 – 계속 info
info
info
info
info
David
John
Mary
Peter
Robert
free
null
null
list
info
info
info
info
David
John
Peter
Robert
null
null
null
list
free
Mary의 삭제
배열을 이용한 연결구조의 구현은 내부적으로 두 개의 리스트를 유지한다. 즉, 프로그래머가 스스로 빈 공간을 관리해야 한다. 27/34 27/34
배열을 이용한 연결구조의 구현 – 계속 배열을 이용한 연결구조 구현의 장점 매번 삽입할 때마다 공간 할당이 이루어지지 않는다. 동적 메모리 할당을 제공하지 않는 프로그래밍 환경에서는 이 방법이 유일한 대안이다. 배열을 이용한 연결구조 구현의 단점 일반적인 연결구조 방식과 달리 삽입 연산의 경우 리스트가 꽉 찬 경우를 고려해야 한다. 0 2
David
1
2
5
4
John
3
4 6
Peter
5 3
Mary
6 -1
-1
Robert
28/34 28/34
ArrayLinkedList의 생성자 public ArrayLinkedList(int capacity){ if(capacity<0) setup(DEF_LIST_CAPACITY); else setup(capacity); } // ArrayLinkedList(int) public void setup(int capacity){ nodes = new ListNode[capacity]; // 빈 노드들의 리스트 생성 for(int i=0;i<capacity;i++){ nodes[i] = new ListNode(); nodes[i].info = null; 미리 모든 노드를 만든다. nodes[i].next = i+1; } // for 새로운 item을 삽입할 때에는 nodes[capacity-1].next = END; 새 노드를 만들지 않고 배열에 } // ArrayLinkedList::setup(int) 만들어 놓은 노드에 item을 삽입한다.
29/34 29/34
SortedArrayLinkedList의 insert 색인
info
next
색인
info
next
0
null
1
0
john
END
1
null
2
1
null
2
2
null
3
2
null
3
3
null
4
3
null
4
4
null
5
4
null
5
5
null
6
5
null
6
6
null
END
6
null
END
list
-1
조건: 리스트가 빈 경우 list == END
int tmp = nodes[free].next; nodes[free].info = item; nodes[free].next = END; list = free; free = tmp;
insert(john);
free
0
size
0
list
0
free
1
size
1
30/34 30/34
SortedArrayLinkedList의 insert 색인
info
next
색인
info
next
0
john
END
0
john
1
1
null
2
1
Robert
END
2
null
3
2
null
3
3
null
4
3
null
4
4
null
5
4
null
5
5
null
6
5
null
6
6
null
END
6
null
END
조건: 리스트 맨 끝에 삽입 loc == END
int tmp = nodes[free].next; nodes[free].info = item; nodes[free].next = END; nodes[prevLoc].next = free; free = tmp;
insert(Robert);
list
0
free
1
size
1
list
0
free
2
size
2
31/34 31/34
SortedArrayLinkedList의 insert 색인
info
next
색인
info
next
0
john
1
0
john
2
1
Robert
END
1
Robert
END
2
null
3
2
mary
1
3
null
4
3
null
4
4
null
5
4
null
5
5
null
6
5
null
6
6
null
END
6
null
END
조건: 일반적인 경우
int tmp = nodes[free].next; nodes[free].info = item; nodes[free].next = loc; nodes[prevLoc].next = free; free = tmp;
insert(mary);
list
0
free
2
size
2
list
0
free
3
size
3
32/34 32/34
SortedArrayLinkedList의 insert 색인
info
next
색인
info
next
0
john
2
0
john
2
1
Robert
END
1
Robert
END
2
mary
1
2
mary
1
3
null
4
3
Bob
0
4
null
5
4
null
5
5
null
6
5
null
6
6
null
END
6
null
END
조건: 리스트 맨 앞에 삽입 prevLoc == END
int tmp = nodes[free].next; nodes[free].info = item; nodes[free].next; = loc; list = free; free = tmp;
insert(Bob);
list
0
free
3
size
3
list
3
free
4
size
4
33/34 33/34
SortedArrayLinkedList의 delete 색인
info
next
색인
info
next
0
John
2
0
John
1
1
Robert
END
1
Robert
END
2
Mary
1
2
null
4
3
Bob
0
3
Bob
0
4
null
5
4
null
5
5
null
6
5
null
6
6
null
END
6
null
END
조건: 일반적인 경우
nodes[prevLoc].next = nodes[loc].next; nodes[loc].info = null; nodes[loc].next; = free; free = loc;
delete(Mary);
list
3
free
4
size
4
list
3
free
2
size
3
34/34 34/34
©copyright 2005
자료구조 및 실습(INA240) NOTE 08
재귀 프로그래밍
한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 재귀 프로그래밍: 프로그래밍 문제를 작은 문제로 분해하여 해결하는 매우 강력하고 유용한 문제 해결 방법이다. 이 때 작은 문제는 원 문제와 동일한 종류이다. 반복(iteration) 대신에 사용할 수 있는 방법 factorial, 조합, 하노이 타워 배열로 구현된 정렬 리스트에서 이진 검색 정렬 연결 리스트에서 역순 출력 정렬 연결 리스트에서 삽입
2/23
재귀(recursion) 재귀 호출: 호출 호출하는 메소드와 호출받는 메소드가 같은 메소드 호출 직접 재귀(direct recursion): 자기 자신을 바로 호출하는 메소드 재귀 간접 재귀(indirect recursion): 둘 이상의 메소드 호출이 최초의 재귀 호출 메소드으로 되돌아 오는 경우 재귀의 예: factorial if n = 0 ⎧1, n! = ⎨ ⎩ n × ( n − 1) × " × 1 if n > 0
예8.1) 4!=4±3±2±1=24 위 factorial의 정의를 다시 표현하면
base case: 재귀가 아닌 경우
if n = 0 ⎧1, n! = ⎨ ⎩ n × ( n − 1)! if n > 0
general case
이와 같이 자신의 작은 버전으로 자신을 정의하는 정의를 재귀 정의(recursive definition)라 한다. 정의 3/23
재귀 프로그래밍
factorial(4)
public static int factorial(int n){ if(n==0) return 1; else return (n * factorial(n-1)); }
4
3 n=4
24
recursive 방법 보통 선택문(if, switch)으로 구현 함수 호출이 많이 이루어진다.
2 n=3
6
public static int factorial(int n){ int val = 1; for(int i=2; i<=n; i++) val = val * i; return val; }
1 n=2
2
0 n=1
1
n=0 1
non-recursive 방법 보통 반복문(for, while)으로 구현
4/23
문제에 대한 재귀적 해결책 어떤 문제를 재귀적으로 해결하고자 할 때 고려해야 하는 네 가지 사항 문제를 같은 종류의 작은 문제로 재귀 정의를 할 수 있는가? 각 재귀호출 때 문제의 크기를 어떻게 줄이는가? base case 역할을 하는 문제의 인스턴스는 무엇인가? 각 재귀호출이 결국에는 base case에 도달할 수 있는가?
5/23
재귀 프로그래밍의 검증 세 가지 질문 base-case question: question 메소드를 종료할 수 있는 비재귀적인 방법이 있나? base-case가 제대로 구현되어 있나? n=0일 때가 base-case이며, 이 때 1을 반환한다. smaller-case question: question 재귀 호출이 원 문제의 작은 버전을 호출하고 있는가? 결국에는 base-case까지 도달하나? Æ 아니면 무한 재귀 재귀호출에서 인자는 n-1을 사용하고 있다. 일련의 재귀 호출은 인자의 값을 하나씩 감소하므로 결국에는 n이 0이된다. general-case question: question 전체적으로 제대로 구현되어 있나? factorial(n-1)이 (n-1)!을 반환해주면 전체적으로 n!을 반환한다.
6/23
search의 재귀 버전 UnsortedList의 search base case (1) element[startPosition]에 찾고자 하는 것이 있으면 true를 반환한다. (2) startPosition==size-1이고 element[startPosition]에 찾고자 하는 것이 없으면 false를 반환한다. general case: 남은 부분(startPosition에서 끝까지)을 검색한다. …
이미 검색한 부분
…
…
검색해야 할 부분
startPosition
7/23
search의 재귀 버전 – 계속 public boolean search(Object item) throws ListUnderflowException { if(isEmpty()) return new ListUnderflowException(“…”); return search(item, 0); } private boolean search(Object item, int startPosition){ if(item.equals(element[startPosition])) return true; else if(startPosition==(size-1)) return false; 사용자는 기존처럼 search(Object item) else return search(item, startPosition+1); 메소드를 호출한다. } public boolean search(Object item) throws ListUnderflowException { if(isEmpty()) return new ListUnderflowException(“…”); for(int i=0; i<size; i++) if(item.equals(element[i])) return true; return false; } …
…
…
1 2 3 8/23
조합(Combination) 순열(permutation): n개 중 r개를 비복원추출하여 어떤 순서로 순열 나열하는 경우의 수 n
Pr = n × ( n − 1) × " × ( n − r + 1)
조합(combination): 순서와 상관없이 n개의 요소에서 r개를 비복원추 조합 n! 출하는 경우의 수 n Pr n
조합의 재귀적 정의
n
Cr =
r!
=
r !( n − r ) !
⎧n ⎪ n C r = ⎨1 ⎪ ⎩ n −1 C r −1 + n −1 C r
if r = 1 if n = r if n > r > 1
( n − 1)! ( n − 1)! + ( r − 1)!( n − r ) ! r !( n − r − 1) ! ⎛ ( n − 1)! 1 ⎞ ( n − 1)! n ⎞ n! ⎛1 = + = = ⎜ ⎟ ⎜ ⎟ ( r − 1)!( n − r − 1) ! ⎝ r n − r ⎠ ( r − 1)!( n − r − 1) ! ⎝ r ( n − r ) ⎠ r !( n − r ) !
Cr =
n −1
C r −1 + n −1 C r =
9/23
조합 – 계속 public static int combination(int n, int r){ if(r==1) return n; else if(n==r) return 1; else return (combination(n-1,r-1)+combination(n-1,r)); }
C(2,1) 2
combination(4,3)
C(3,2) C(2,2) 3
1
C(3,3) 1
10/23 10/23
하노이 타워
begin peg
aux peg
end peg
peg 1
peg 2
peg 3
제약 (1) 한번에 하나의 링만 움직일 수 있다. (2) 작은 링 위에 큰 링을 올려놓을 수 없다. 힌트
11/23 11/23
하노이 타워 – 계속 aux peg
begin peg
end peg
begin peg
aux peg
end peg
12/23 12/23
하노이 타워 – 계속 하노이 타워 알고리즘 단계 1. n-1개의 링을 peg 1(시작 peg)에서 peg 2(보조 peg)로 옮겨라. 단계 2. n번째 링을 peg 3(목적 peg)으로 옮겨라. 단계 3. peg 2(보조 peg)에 있는 n-1개의 링을 peg 3(목적 peg) 으로 옮겨라. public static void doTowers(int numRings, int begPeg, int auxPeg, int endPeg){ if(numRings>0){ doTowers(numRings-1, begPeg, endPeg, auxPeg); system.out.println(“Move ring from peg ”+ begPeg + “ to peg ” + endPeg); doTowers(numRings-1, auxPeg, begPeg, endPeg); } }
13/23 13/23
하노이 타워 – 계속 doTowers(3,1,2,3) 4 doTowers(2,1,3,2)
Move ring from peg 1 to 3
doTowers(1,1,2,3)
Move ring from peg 1 to 2
doTowers(2,2,1,3) 2 doTowers(1,3,1,2)
1 Move ring from peg 1 to 3
3 Move ring from peg 3 to 2 6
doTowers(1,1,2,3)
Move ring from peg 2 to 3
doTowers(1,2,3,1)
7 Move ring from peg 1 to 3
5 Move ring from peg 2 to 1
14/23 14/23
하노이 타워 – 계속 1 Move ring from peg 1 to 3 2 Move ring from peg 1 to 2 3 Move ring from peg 3 to 2 4 Move ring from peg 1 to 3 5 Move ring from peg 2 to 1 6 Move ring from peg 2 to 3 7 Move ring from peg 1 to 3
15/23 15/23
이진 검색
public boolean search(Object item){ if(isEmpty()) return new ListUnderflowException(“…”); Comparable x = (Comparable)item; return search(x, 0, size-1); } private boolean search(Comparable item, int first, int last){ if(first>last) return false; else{ int mid = (first+last)/2; int comp = item.compareTo(element[mid]); if(comp==0) return true; else if(comp<0) return search(item, first, mid-1); else return search(item, mid+1,last); } }
Base case (1) first>last Æ false를 반환 (2) item.compareTo(element[mid])==0 Æ true를 반환 General case (1) item.compareTo(element[mid])<0 Æ search(first,mid-1) (2) item.compareTo(element[mid])>0 Æ search(mid+1,last)
16/23 16/23
연결 리스트에서 재귀 정렬 연결 리스트에서 역순으로 출력 list
A
B
2
C
D
E
1 이것을 출력: E,D,C,B, A
이 부분을 역순으로 출력: E,D,C,B
public void PrintReverse() { revPrint(list); } private void revPrint(ListNode node){ if(node != null){ revPrint(node.next); System.out.println(“ ” + node.info); } } 17/23 17/23
연결 리스트에서 재귀 – 계속 private ListNode insert(ListNode subList, Comparable item){ if(subList==null || item.compareTo(subList.info)<0){ ListNode newNode = new ListNode(); newNode.info = item; newNode.next = subList; 정렬 리스트에서 삽입 return NewNode; } Base Case: else{ (1) 부분 리스트가 비어있으면 subList.next = insert(subList.next, item); 빈 리스트에 삽입 return subList; (2) 삽입하고자 하는 요소가 } 부분 리스트의 첫 요소보다 } 작으면 리스트 맨 앞에 삽입 public void insert(Object item){ General Case: if(item==null) insert(sublist.next, item) return new NullPointerException(“…”); Comparable x = (Comparable)item; list = insert(list, x); }
18/23 18/23
연결 리스트에서 재귀 – 계속 list
6
11
2
20
11
1
insert(list,15)
15
20
15
20
15
20
1 6
6
11
20 11
insert(subList.next,15) 2 11
list
20
6
insert(subList.next,15) 3 20
15
11
20
19/23 19/23
재귀의 동작 원리 public static int factorial(int n){ if(n==2) return 2; else return (n * factorial(n-1)); } factorial(3)/ call factorial(2)
public static void main(String[] args){ int result = factorial(4); System.out.println(“4! = ”+result); }
parameter
2
Return Address 1000 return 2
factorial(4)/ call factorial(3)
Return Value
?
parameter
3
2
Return Address 1000 return 3*2 call
return main/ call factorial(4)
runtime stack
Return Value
?
parameter
4
Return Address
100
Return Value
?
6
return 4*6 24
20/23 20/23
재귀의 제거 재귀 방식으로 구현된 메소드를 재귀 호출을 사용하지 않도록 바꾸는 방법 방법 1. 반복문 사용 모든 경우에 반복문을 바꿀 수 있는 것은 아니다. 꼬리 재귀(tail recursion)이면 쉽게 바꿀 수 있다. 재귀 방법 2. 스택 사용 리스트의 역순 출력: 리스트의 각 노드를 차례대로 방문하면서 노드의 요소 값을 스택에 push한 다음에 스택에서 하나씩 pop 하여 출력하면 역순으로 출력할 수 있다.
21/23 21/23
재귀 Æ 반복 꼬리 재귀: 메소드 내에 모든 재귀 호출이 메소드의 마지막 문장인 경우 base case가 되면 반복이 종료되도록 반복문을 구성하여 쉽게 재귀를 제거할 수 있음 이 때 루프 변수는 재귀 호출에서 값이 변하는 인자 꼬리 재귀가 아니면 쉽게 바꿀 수 없다. boolean search(Comparable item, int start){ if(item.compareTo(element[start])==0) return true; else if(start==size-1) return false; else return search(item, start+1); }
boolean search(Comparable item){ int loc = 0; boolean found = false; while(loc<size && !found){ if(item.compareTo(element[loc])==0) found = true; else loc++; } return found; }
22/23 22/23
재귀의 사용 여부 두 가지 측면: 명확성, 효율성 명확성: 재귀가 보통 알고리즘을 이해하기가 더 쉽다. 예8.2) 역순으로 정렬 연결 리스트 출력 효율성: 재귀가 당연히 공간과 시간 측면에서 모두 나쁘다. 절대적인 것은 아님: 문제, 컴퓨터, 컴파일러에 따라 다를 수 있다. 예8.3) factorial(n): n+1개의 stack frame이 필요 Æ O(n) 문제가 본질적으로 재귀에 맞지 않을 수 있다. Æ 조합: 같은 계산이 무수히 반복 Æ O(2N) C(5,3) C(4,3)
C(4,2) C(3,1)
C(3,2) C(2,1)
C(2,2)
C(3,2) C(2,1)
C(3,3)
C(2,2)
23/23 23/23
자료구조 및 실습(INA240) NOTE 09
©copyright 2005
이진 검색 트리
한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 트리 개요 이진 트리 개요 순회 방법 이진 검색 트리 ADT 검색 삽입 삭제 순회 이진 트리 균형 맞추기
2/30
트리 구조 선형 연결 리스트는 성능 측면에서는 배열을 이용한 리스트 구현에 비해 우수하지 못하지만 공간 활용 측면에서는 우수하다. 선형 연결 리스트의 또 다른 단점은 임의 접근을 할 수 없으므로 정렬된 리스트에서도 이진 검색을 할 수 없다는 것이다. 선형 연결 리스트에서 하나의 노드는 오직 하나의 다른 노드만 가리킬 수 있다. 트리(tree)는 트리 루트(root)라고 하는 유일한 시작 노드를 루트 노드 가지며, 각 노드는 여러 개의 자식 노드를 가질 수 있는 구조로서, 구조 루트에서 각 노드까지의 경로가 유일한 구조를 구조 말한다. 트리는 재귀 구조(recursive structure)이다. 구조 각 노드를 기준으로 그 노드가 루트가 되는 부분트리(subtree)를 부분트리 만들 수 있다. 3/30
트리 구조 – 계속 root (루트 노드) internal node, intermediate node (중간 노드, 내부 노드)
트리가 아님 Æ 그래프
subtree (부분 트리)
leaf node (단말 노드)
루트: 부모가 없는 노드 단말: 자식이 없는 노드
루트에서 각 노드까지의 경로가 유일해야 한다. 즉, 부모가 하나이어야 한다.
4/30
트리 용어 루트 노드(root node): 부모가 없는 유일한 노드 단말 노드(leaf node): 자식이 없는 노드 중간 노드(internal node): 단말 노드를 제외한 모든 노드 형제 노드(sibling node): 부모가 같은 노드 조상 노드(ancestor node): 노드에서 루트 노드까지 경로 상에 있는 모든 노드 후손 노드(descendant node): 노드에서 단말 노드까지 경로 상에 있는 모든 노드 노드의 레벨: 루트 노드로부터의 거리 루트 노드는 0 레벨에 위치하며, 루트로부터 각 노드까지의 경로의 길이가 그 노드의 레벨이 된다. 트리의 높이: 트리의 루트로부터 가장 먼 노드까지의 경로의 길이
5/30
이진 트리 이진 트리(binary tree): 트리 각 노드가 최대 두 개의 자식 노드만을 가질 수 있는 트리 각 레벨 l에 있을 수 있는 최대 노드의 수: 2l 높이가 h인 트리에 있을 수 있는 최대 노드의 수: 2h+1-1 2h+1 − 1 h+1 2 + 2 +" + 2 = = 2 −1 2 −1 0
1
root
level 0
A
level 1 B
C
level 2 D
E
F
level 3 G
h
height(depth): 3
N개의 노드로 만들 수 있는 트리의 최소 높이: ⎢⎣ log 2 N ⎥⎦ 2h ≤ N < 2h+1 (노드의 수가 2h일 때 트리의 높이는 h) 2h − 1 < 2h < 2h+1 − 1 h ≤ log2 N < h + 1
H
I
J leaf
G는 B의 자손 노드 C는 I의 조상 노드 G는 D의 왼쪽 자식(left child) H는 D의 오른쪽 자식(right child) G와 H는 형제 노드(sibling)
6/30
이진 트리의 종류
포화 이진 트리(full binary tree):
완전 이진 트리(complete binary tree):
모든 단말 노드가 같은 레벨에 있으며, 모든 중간 노드는 두 개의 자식 노드를 가지는 경우
마지막 레벨을 제외하면 포화 이진 트리 이며, 촤하위 레벨의 단말 노드들은 왼쪽에 서부터 채워진 형태로 되어 있는 경우
단말 노드의 수가 n이면 전체 노드의 수는 N=2n-1이다.
완전 이진 트리에서 중간 노드의 자식 수가 2일 때 단말 노드의 수가 n이면 전체 노드의 수는 N=2n-1이다. 7/30
이진 검색 트리 이진 검색 트리(binary search tree)란 트리 각 노드의 키 값이 왼쪽 자손 노드들의 키 값보다는 항상 크고, 오른쪽 자손 노드들의 키 값보다는 항상 작은 이진 트리를 말한다. 60
45 (왼쪽 부분 트리) left subtree
63
41
40
55
43
62
(오른쪽 부분 트리) right subtree
65
50
왼쪽 부분 트리의 노드의 키 값<루트 노드의 키 값<오른쪽 부분 트리의 노드의 키 값
8/30
이진 검색 트리의 예 60
height: 9 편향 이진 트리 (skewed binary tree)
40
50
41 45
63
40
55
43 45
41
55
62
65
45
62 50
40
43
50
43 height: 3
60
65
60
height: 4 41
55
63 62 63 65
9/30
이진 트리 순회 순회(traversing): 트리에 있는 모든 노드를 방문하는 것 순회 트리에 있는 모든 노드를 순회할 때 가장 널리 사용되는 방법 전위(preorder) 순회: 루트 노드를 방문 Æ 왼쪽 부분 트리에 있는 전위 노드들을 방문 Æ 오른쪽 부분 트리에 있는 노드들을 방문 중위(inorder) 순회: 왼쪽 부분 트리에 있는 노드들을 방문 Æ 루트 중위 노드를 방문 Æ 오른쪽 부분 트리에 있는 노드들을 방문 후위(postorder) 순회: 왼쪽 부분 트리에 있는 노드들을 방문 Æ 후위 오른쪽 부분 트리에 있는 노드들을 방문 Æ 루트 노드를 방문 이들 순회 방법은 루트 노드를 방문하는 순서에 의해 구분되며, 구분 위 세 가지 순회 방법은 공통적으로 왼쪽 부분 트리는 항상 오른쪽 부분 트리보다 먼저 방문한다. 중위 순회 방법으로 순회하면 작은 값부터 순서대로 방문하게 된다.
10/30 10/30
이진 트리 순회의 예 60
45
41
Preorder: 60 45 41 55 63 65 Inorder: 41 45 55 60 63 65 Postorder: 41 55 45 65 63 60
63
55
65
50
40
Preorder: 50 40 45 43 47 55 52 62 60 65 Inorder: 40 43 45 47 50 52 55 60 62 65 Postorder: 43 47 45 40 52 60 65 62 55 50
55
45
43
52
47
62
60
65
11/30 11/30
BST Interface Import java.util.Iterator; public interface BST{ int INORDER = 1; int PREORDER = 2; int POSTORDER = 3; boolean isEmpty(); boolean isFull(); void clear(); int numOfNodes(); boolean search(Object item); Object retrieve(Object item); void insert(Object item); boolean delete(Object item); Iterator iterator(int type); }
이진 검색 트리는 중복 요소의 추가를 허용하지 않는다. 참조 방식으로 구현한다.
순회 방법
12/30 12/30
public class BinarySearchTree implements BST{ protected class BSTNode{ public Object info; public BSTNode left; public BSTNode right; } protected class TreeIterator implements Iterator{ LinkedQueue traverseQueue; public TreeIterator(int type){…} public boolean hasNext(){…} public Object next(){…} public void remove(){…} private void preOrder(BSTNode node){…} node.left private void inOrder(BSTNode node){…} private void postOrder(BSTNode node){…} } protected BSTNode root = null; protected int numOfNodes = 0; public BinarySearchTree(){} public boolean isEmpty(){ return (root==null); } public boolean isFull(){ return false; } void clear() { … } public int numOfNodes(){ return numOfNodes; } public boolean search(Object item){…} public Object retrieve(Object item){…} public void insert(Object item){…} public boolean delete(Object item){…} public Iterator iterator(int type){ return new TreeIterator(type); } }
node.right node.info
13/30 13/30
NumOfNodes 재귀적 방법으로 public int size(){ return size(root); } private int size(BSTNode node){ if(node == null) return 0; // leaf node else return(size(node.left)+size(node.right)+1); }
Tip. 트리는 재귀적 구조이므로 재귀적 방법으로 구현하는 것이 쉽다.
14/30 14/30
NumOfNodes 비재귀적 방법으로 E F C
C
E
F
A
count = 0; A 트리가 empty가 아니면 다음을 스택을 스택 하나 생성 루트 노드를 스택에 push 스택이 empty가 아니면 다음을 반복 현재 노드를 스택 top에 있는 노드를 가리키도록 함 스택에서 노드를 하나 pop한 다음에 count를 1 증가 만약 현재 노드가 왼쪽 자식이 있으면 그 자식을 스택에 push 만약 현재 노드가 오른쪽 자식이 있으면 그 자식을 스택에 push return count;
15/30 15/30
search 재귀 방법으로 private boolean search(Comparable item, BSTNode node){ if(node == null) return false; int comp = item.compareTo(node.info); if(comp<0) return search(item, node.left); else if(comp>0) return search(item, node.right); else return true; } public boolean search(Object item){ if(isEmpty()) throw new TreeUnderflowException(“…”); if(item==null) throw new NullPointerException(“…”); Comparable x = (Comparable)item; return search(x, root); }
60
45
63
41
55
62
65
전형적인 이진 검색 40
43
50
16/30 16/30
search 반복 방법으로 found = false; 트리가 비어 있으면 return false; moreToSearch = true; node = root; found가 true이거나 moreToSearch가 false일 때까지 다음을 반복 node와 item을 비교 같으면 return true; item이 node보다 작으면 node = node.left; item이 node보다 크면 node = node.right; moreToSearch = (node != null) return found
17/30 17/30
insert 1 insert(5)
2 insert(9)
5
5 insert(8)
9 4
insert(3)
5
3
3
9
7
6
7
9
3 12
9
4
12
7
5 9
7
5
7
insert(12)
8 3
insert(4)
5
5
9
8
insert(6)
5
5
3 insert(7)
7
6 3
8
6
8
9
12
7
8
항상 단말노드로 추가된다. 삽입되는 순서가 트리의 모습을 결정한다.
18/30 18/30
insert public void insert(Object item){ if(item==null) throw new NullPointerException(“…”); Comparable x = (Comparable)item; root = insert(x, root); } private BSTNode insert(Comparable item, BSTNode node){ if(node == null){ BSTNode newNode = new BSTNode(); newNode.info = item; newNode.left = null; newNode.right = null; numOfNodes++; return newNode; } int comp = item.compareTo(node.info); if(comp<0) node.left = insert(item, node.left); else if(comp>0) node.right = insert(item, node.right); else node.info = item; return node; } 19/30 19/30
delete delete(8)
delete(2)
5
3
9
4
3
12
7
6
5
5
2
9
4
8
7
6
5
9
4
12
3
12
7
6
4
9
3
8
12
7
6
단말 노드의 삭제
자식이 하나인 노드의 삭제
바로 삭제
남은 자식과 노드를 교체
8
20/30 20/30
delete – 계속 delete(9) 5
3
9
4
알고리즘
5
3
8
12
7
4
8
6
삭제할 노드를 찾는다. 삭제할 노드가 단말노드이면 노드 삭제 삭제할 노드가 단말노드가 아닌 경우 오른쪽 자식 노드가 없으면 왼쪽 자식 노드를 한 단계 위로 왼쪽 자식 노드가 없으면 오른쪽 자식 노드를 한 단계 위로 자식이 둘 다 있으면 predecessor를 찾아 이것을 삭제할 노드의 값과 바꿈 predecessor 노드를 삭제
12
7
6
자식이 둘인 노드의 삭제
왼쪽 부분 트리 중 가장 큰 노드(predecessor)와 교체 - 이 노드는 항상 왼쪽 부분 트리의 모든 노드보다는 크고, 오른쪽 부분 트리의 모든 노드보다는 작은 노드이다. 오른쪽 부분 트리의 가장 작은 노드와 교체 가능
21/30 21/30
delete – 계속 5
5
3
12
4
3
7
15
9
6
8
5
13
9
4
17
3
7
15
9
6
13
9
4
17
7
6
15
8
13
17
8
22/30 22/30
public boolean delete(Object item){ if(isEmpty()) throw new TreeUnderflowException(“…”); if(item==null) throw new NullPointerException(“…”); Comparable x = (Comparable)item; deleteSuccessful = false; root = delete(x, root); if(deleteSuccessful){ numOfNodes--; return true; } else return false; } private BSTNode delete(Comparable item, BSTNode node){ if(node == null) return node; int comp = item.compareTo(node.info); if(comp<0) node.left = delete(item, node.left); else if(comp>0) node.right = delete(item, node.right); else{ deleteSuccessful = true; node = deleteNode(node); } return node; } 23/30 23/30
private BSTNode deleteNode(BSTNode node){ if(node.left==null&&node.right==null) return null; // 단말노드 else if(node.right==null) return node.left; // 오른쪽 자식이 없는 노드 else if(node.left==null) return node.right; // 왼쪽 자식이 없는 노드 else{ // 자식이 모두 있는 노드 Object predecessor = getPredecessor(node.left); node.info = predecessor; node.left = delete((Comparable)predecessor, node.left); return node; } } private getPredecessor(BSTNode node){ … } 10
5
12
8
3
7
8
5
3
12
7
24/30 24/30
Traverse 세 가지 순회 방법을 제공하는 반복자 클래스 사용 지정된 방법으로 미리 순회하여 순회한 순서로 큐에 노드의 요소를 추가함
protected class TreeIterator implements Iterator{ LinkedQueue traverseQueue; public TreeIterator(int type){ traverseQueue = new LinkedQueue(); switch(type){ case INORDER: inOrder(root); return; case PREORDER: preOrder(root); return; case POSTORDER: postOrder(root); return; } // switch } public boolean hasNext() { return !traverseQueue.isEmpty(); } public Object next(){ return traverseQueue.deq(); } public void remove(){ return new UnsupportedOperationException(“…”); } private void preOrder(BSTNode node){ … } private void inOrder(BSTNode node){ if(node!=null){ if(node.left!=null) inOrder(node.left); traverseQueue.enq(node.info); if(node.right!=null) inOrder(node.right); } } private void postOrder(BSTNode node){ … } } 25/30 25/30
비교 BST*
배열 리스트
연결 리스트
O(1)
O(N)
O(1)
search
O(log2N)
O(log2N)
O(N)
retrieve Find process Total
O(log2N) O(1) O(log2N)
O(log2N) O(1) O(log2N)
O(N) O(1) O(N)
insert Find process Total
O(log2N) O(1) O(log2N)
O(log2N) O(N) O(N)
O(N) O(1) O(N)
delete Find process Total
O(log2N) O(log2N) O(log2N)
O(log2N) O(N) O(N)
O(N) O(1) O(N)
constructor
*. 균형 트리일 경우에만 O(log2N)
26/30 26/30
트리의 균형 맞추기 기본적인 생각 단계 1. 트리 정보를 배열에 저장한 다음에 세 가지 방법: inorder, preorder, postorder 단계 2. 이 정보를 이용하여 트리를 재구성 아이템을 추가할 때마다 균형을 맞추기 위해 트리를 재구성하는 것은 비효율적이다. 그러면 언제? 트리의 현재 높이와 현재 노드의 수로 구성할 수 있는 최적의 트리의 높이의 차이가 너무 크면 기준은 응용에 따라 결정
27/30 27/30
트리의 균형 맞추기 – 계속 3
10
5
12
8
3
7
10
5
5
7
8
12
8
3
3
7
5
8
12
7
10
10
inorder: 3 5 7 8 10 12 12
preorder: 10 5 3 8 7 12 postorder: 3 7 8 5 12 10
inorder
preorder
postorder
28/30 28/30
트리의 균형 맞추기 – 계속 7
8 10 5 5
10
12 3 8
3
5
12
7
10
7
8
3
12
방법: inorder에서 중간 노드부터 삽입 10 12
low=0, high=6, mid=3
3
5
7
8
3
5
7
low=0, high=2, mid=1
inorder: 3 5 7 8 10 12 3 7 10 12
low=4, high=5 Æ high, low순으로 삽입
29/30 29/30
트리의 균형 맞추기 – 계속 public void BalanceTree(){ inorder로 Object 배열 nodes에 BST의 각 노드 값을 차례로 삽입 BalanceTree(0,numOfNodes,nodes); } private void BalanceTree(int low, int high, Object[] nodes){ low==high이면 nodes[low]를 삽입 (low+1)==high이면 nodes[high]와 nodes[low]를 모두 삽입 위 두 경우가 아니면 mid = (low+high)/2 nodes[mid]를 삽입 BalanceTree(low, mid-1, nodes); BalanceTree(mid+1, high, nodes); }
30/30 30/30
©copyright 2005
자료구조 및 실습(INA240) NOTE 10
우선순위 큐: 힙 AVL 트리 한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 이진 트리 구조의 유용한 자료 구조 우선순위 큐: 힙(heap) AVL 트리: 균형 트리
2/30
우선순위 큐 일반적으로 큐는 FIFO 구조를 가진다. 우선순위 큐는 큐에 들어가는 순서와 상관없이 어떤 우선순위에 의해 나가는 순서가 결정되는 큐를 말한다. 우선순위는 응용에 따라 다르며, 들어오는 순서가 빠를 수록 높은 우선순위를 가질 경우에는 FIFO 큐도 우선순위 큐로 볼 수 있다. 우선순위 큐를 구현하는 방법 비정렬 리스트
정렬 리스트
이진 검색 트리
enq
O(1)
O(N)
O(log2N)*
deq
O(N)
O(1)
O(log2N)
모두 최악의 경우 비용이다. *. 평균 비용이다. 이진 검색 트리의 현재 모습에 따라 비용이 다를 수 있다.
3/30
힙 힙(heap)은 이진 트리를 이용하여 우선순위 큐를 구현하는 자료구조로서, 다음 두 가지 특성을 만족해야 한다. 모양 특성: 특성 항상 완전 이진 트리를 유지해야 한다. 순서 특성: 특성 각 노드의 값은 그것의 자식 노드들의 값보다는 크거나 같다. 따라서 항상 루트 노드가 가장 큰 값을 가지게 된다. J
J heap
H
I
D
B
G
C
E
F
I
A
H
F
B
G
C
E
A
D
4/30
힙: dequeue 힙에서 dequeue는 항상 루트를 제거하게 된다. 쉽게 제거할 수 있지만 힙은 항상 완전 이진 트리를 유지해야 하므로 완전 이진 트리가 되도록 트리를 재조정해야 한다. 트리가 완전 이진 트리가 유지되도록 하기 위해서는 가장 마지막 레벨에 있는 단말 노드들 중에서 가장 오른쪽에 있는 노드를 루트로 옮겨야 한다. J
D
I
H
F
B
G
C
I
E
A
H
F
D
B
G
E
A
C
5/30
힙: dequeue – 계속 이 경우 모양 특성은 만족되지만 순서 특성을 만족하지 못한다. 순서 특성이 만족되도록 자식 노드 값과 교체해야 하며, 이런 교체는 트리가 순서 특성을 만족할 때까지 반복된다. 이런 재조정을 reheapdown이라 한다. reheapdown D
I
I
F
B
H
G
C
E
G
A
F
B
H
D
E
A
C
6/30
힙: enqueue 힙에서 enqueue는 완전 이진 트리를 유지하기 위해서는 항상 가장 마지막 레벨에 가장 오른쪽에 추가되어야 한다. 이렇게 추가하면 모양 특성은 만족되나 삽입되는 값에 따라 순서 특성은 만족되지 않을 수 있다. P
P
L
N
E
B
F
C
K
L
G
A
N
E
B
F
C
A
K
G
M
7/30
힙: enqueue – 계속 dequeue와 비슷하게 순서 특성을 만족하도록 부모와 값을 교체해야 한다. 이런 교체는 트리가 순서 특성을 만족할 때까지 반복되어야 하며, 이런 재조정을 reheapup이라 한다. reheapup
P
P
L
N
E
B
F
C
A
K
M
M
G
N
E
B
L
C
A
K
G
F
8/30
힙의 구현 힙을 구현할 때 트리의 마지막 레벨에 가장 오른쪽 단말노드를 쉽게 접근할 수 있어야 하며, 노드에서 그 노드의 자식 노드를 접근할 수 있을 뿐만 아니라 부모 노드를 접근할 수 있어야 한다. 연결구조를 이용하여 구현하고자 하면 노드를 나타내는 정보에 부모에 대한 연결을 추가해야 한다. 힙은 완전 이진 트리를 항상 유지하므로 이렇게 연결구조로 구현하는 것보다 배열로 구현하는 것이 효과적이다. 배열을 이용한 구현에서는 부모와 자식에 대한 연결을 명백하게 유지하지 않는다. 대신에 노드가 저장되어 있는 배열의 색인을 이용하여 부모와 자식 노드가 저장되어 있는 배열의 색인을 계산할 수 있다.
9/30
P
L
E
B
N
F
C
K
G
0
P
1
L
2
N
3
E
4
F
5
K
6
G
7
B
8
C
부모의 위치: (index-1)/2 왼쪽 자식의 위치: (index*2)+1 오른쪽 자식의 위치: (index*2)+2
배열을 이용한 이진 트리의 표현은 한 노드에서 자식 노드들은 물론 부모 노드도 쉽게 접근할 수 있다. 배열을 이용한 완전 이진 트리의 표현은 중간에 사용되지 않는 항이 없다. 마지막 레벨의 가장 오른쪽 단말 노드의 위치는 트리에 있는 노드의 수를 통해 얻을 수 있다. 10/30 10/30
힙의 구현: PriorityQueue, Heap
public interface PriorityQueue{ boolean isEmpty(); boolean isFull(); void enq(Object item); Object deq(); }
public class Heap implements PriorityQueue{ private static final int MAX_HEAP_SIZE = 50; private Object[] elements; private int lastIndex = -1; public Heap(){ setup(MAX_HEAP_SIZE); } public Heap(int capacity){ if(capacity>0) setup(capacity); else setup(MAX_HEAP_SIZE); } private void setup(int capacity){ elements = new Object[capacity]; } public boolean isEmpty(){ return (lastIndex == -1); } public boolean isFull(){ return (lastIndex == elements.length-1); } … } 11/30 11/30
Enqueue public void enq(Object item) throws QueueOverflowException{ if(isFull()) throw new QueueOverflowException(“…”); if(item==null) throw new NullPointerException(“…”); Comparable o = (Comparable)item; lastIndex++; reheapUp(o); } private void reheapUp(Comparable item){ int hole = lastIndex; int parent = (hole-1)/2; while((hole>0) && (item.compareTo(elements[parent])>0)){ elements[hole] = elements[parent]; hole = parent; parent = (hole-1)/2; } L elements[hole] = item; }
P
N
E
B
F
C
A
K
hole
G
M
item
12/30 12/30
Dequeue public Object deq() throws QueueUnderflowException{ if(isEmpty()) throw new QueueUnderflowException(“…”); Comparable toMove = (Comparable)elements[lastIndex]; Object tmp = elements[0]; lastIndex--; reheapDown(toMove); return tmp; } private void reheapDown(Comparable item){ int hole = 0; int nextHole; nextHole = findNextHole(hole, item); while(nextHole != hole){ elements[hole] = elements[nextHole]; hole = nextHole; nextHole = findNextHole(hole, item); } elements[hole] = item; }
hole nextHole M
N
E
B
L
C
A
K
G
F
13/30 13/30
Dequeue
private int findNextHole(int hole, Comparable item){ int left = hole*2+1; int right = hole*2+2; Comparable leftChild = null; // hole이 자식이 없는 경우 if(left>lastIndex){ return hole; } // hole이 왼쪽 자식밖에는 없는 경우 else if(left==lastIndex){ if(item.compareTo(elements[left])>0) return hole; else return left; } // hole이 왼쪽, 오른쪽 자식이 모두 있는 경우 else{ leftChild = (Comparable)elements[left]; if(leftChild.compareTo(elements[right])>0){ if(item.compareTo(leftChild)>0) return hole; else return left; } else{ if(item.compareTo(elements[right])>0) return hole; else return right; } } } 14/30 14/30
힙 VS. 다른 우선순위 큐 구현 이진 검색 트리
힙
정렬리스트 (연결구조)
균형
편향
enq
O(log2N)
O(N)
O(log2N)
O(N)
deq
O(log2N)
O(1)
O(log2N)
O(N)
이진 검색 트리가 균형트리일 경우에는 힙과 성능이 같다. 이진 검색 트리는 삽입되는 순서에 따라 최악의 경우에는 편향 트리가 될 수 있다. 하지만 힙은 이진 검색 트리와 달리 항상 비용이 일정하다.
15/30 15/30
AVL 트리(균형트리) AVL(Adelson-Velskii and Landis) 트리: 트리 각 노드의 왼쪽 서브트리의 높이와 오른쪽 서브트리의 높이의 차이가 1이하인 이진 검색 트리 균형을 저렴한 비용으로 항상 유지할 수 있다면 기본 이진 검색 트리의 문제점을 해소할 수 있다. 이진 트리의 한 노드가 AVL 성질을 만족하는지 검사하는 방법 가장 긴 왼쪽 경로의 길이와 가장 긴 오른쪽 경로의 길이를 비교 두 경로의 차이를 균형 인수(balance factor)라 한다. 인수 2
60
45
1
63
60
45
0 41
55
61
AVL Tree
41 non AVL Tree 16/30 16/30
AVL 트리: 삽입 삽입은 이진 검색 트리에서 삽입과 같다. 그러나 삽입이 AVL 트리의 성질을 만족하지 못하면 트리의 균형이 깨질 수 있다. 핵심. 루트에서 삽입된 위치까지의 경로에 있는 노드들만 균형 인수가 변경될 수 있다. 이 노드들의 부분 트리만 변경된다. 재균형은 삽입된 위치부터 루트까지의 경로를 따라 올라가면서 균형 인수를 갱신한다. 갱신하는 도중에 AVL 성질을 위배하는 노드를 만나면 이 노드의 부분트리를 재조정한다. 한번 재조정을 하면 그것으로 트리는 다시 AVL 성질을 만족한다. 이것은 삽입되기 전에 트리는 AVL 성질을 만족하고 있었고, 삽입된 후에 노드들의 균형 인수 값은 최대 하나 증가하거나 하나 감소할 수 있기 때문이다.
17/30 17/30
AVL 트리: 삽입 – 계속 부분 트리를 재조정해야 하는 노드 x를 기준으로 삽입된 노드의 위치는 다음 네 가지로 분류할 수 있다. LL: x의 왼쪽 자식의 왼쪽 부분 트리에 삽입된 경우 RR: x의 오른쪽 자식의 오른쪽 부분 트리에 삽입된 경우 LR: x의 왼쪽 자식의 오른쪽 부분 트리에 삽입된 경우 RL: x의 오른쪽 자식의 왼쪽 부분 트리에 삽입된 경우 2
2
-2 A
A
A -1
1 B C
LL
A 1
-1 B
B RR
-2
C
B C LR
C RL
18/30 18/30
LL, RR 회전 트리의 균형은 노드들을 회전(rotate)하여 맞춘다. 회전 LL과 RR의 경우에는 단일 회전으로 균형을 맞출 수 있지만 LR과 RL의 경우에는 이중 회전을 해야 균형을 맞출 수 있다. 2
0
A
B
A
1 B
T1
-1
A
T3
B 0
0
B
A
T1
T1
T2
0
-2
T2
T2
T3
T1
T3
LL Rotate
T3
T2
RR Rotate
19/30 19/30
LR 회전 2
2
A
2 A
A
-1
-1 B
-1 B
1
B -1 C
C
C T4 T1 T2
T2
T3
0
0
0
C
C 0
0 B
T4 T1
T3
C
0
A
-1 B
T1
1
A
T2
T3
0 B
T4
T1
A
T2
T3
T4
20/30 20/30
RL 회전 -2
-2
-2
A
A
A
1 1
1
B
B
-1
B 1
C
C
C T1
T1 T4
T2
T3
T2 0
0
0
C
C 0
0 A
T4
T3
C
1
B
0
0 A
B
T2
T1
-1 A
T3
T4
T1
B
T2
T3
T4
21/30 21/30
AVL 트리: 삽입 예 -1
1
15
0
15
-2
45
15
10
1
45 0
5
5
0 0
45
15 15
1
0 30
10
7
0 30
45
2
30
LR
2 15
5
1 15
2
30
-1
0
0 45
5
2
5
RR
-1
0
30
15
30
1
0 30 15
2
30
2
LL
0 2
0 45 0 15
5 5
2 2
30
30
10
10
45
45
45 7
12
22/30 22/30
AVL 트리: 삽입 예 – 계속 2 1
15
10 0
15
-2 5
RL
30
0 15
1
0
7 7
30
1 2
10
45
5 5
-1
10
8
12
30
45 15 2
7
12
14
2 2
8
12
-1 7
30
8
-1 5
2
10
8
45
LR
12
14
23/30 23/30
AVL 트리: 삭제 삭제도 일반 이진 검색 트리에서 삭제를 먼저 수행한다. 삭제도 삽입과 마찬가지로 AVL 트리의 성질을 위배할 수 있다. 삽입의 경우에는 첫 번째 위배된 노드를 기준으로 재조정하면 전체 트리의 균형이 이루어지지만 삭제의 경우는 한번 재조정을 하여도 여전히 트리가 AVL 트리의 성질을 위배할 수 있다. 따라서 루트까지 계속 검사하면서 여러 번 재조정해야 하는 경우가 존재한다. 단말노드가 삭제될 삭제 경우에는 삭제된 노드의 부모노드부터 루트까지의 경로에 있는 노드들만 균형 여부를 검사한다. 자식이 하나인 중간노드가 삭제될 삭제 경우에는 삭제된 노드의 부모노도부터 루트까지 균형 여부를 검사한다. 자식이 둘인 중간 노드가 삭제될 삭제 경우에는 중간노드와 대체되는 노드의 부모노드부터 루트까지 균형 여부를 검사한다.
24/30 24/30
45
AVL 트리: 삭제 – 고려사항1 삭제된 노드의 부모노드부터 루트 노드까지 거슬러 올라가면서 균형인수를 재조정할 때 다음을 고려한다. 경우 1. 노드의 균형인수가 0으로 바뀌면 그것의 부모노드의 균형인수도 변경되어야 한다. (바뀌기 전 인수 값은 1 또는 -1) 경우 2. 노드의 균형인수가 1 또는 -1로 바뀌면 그것의 부모노드의 균형인수는 변경되지 않는다. (바뀌기 전 인수 값은 0) 경우 3. 노드의 균형인수가 2 또는 -2로 바뀌면 그 위치에서 재조정 해야 하며, 재조정되면 그 노드의 균형인수는 0이 된다. 따라서 경우 1과 마찬가지로 부모노드의 균형인수도 변경되어야 한다. 0
-1
경우 1.
0
0
1
0
경우 2.
0
0
0
1 0
0
25/30 25/30
AVL 트리: 삭제 – 고려사항2 삽입과 전혀 다른 형태 존재 1
2
15
15 0
0 10 0
-1 10
LL
10
20
1
0
0
5
0 12
5
0
12
5 -1 10
5
0
20 0 15
0 15
-1
0
10
20 0 25
20
RR
0
12 1
-2
10 0
15
0 25
25 0 15
26/30 26/30
AVL 트리: 삭제 예 -1 15 -1
0
30
8
5
10
17
50
25
7
20
10
25
17
60
40
7
50
20
8
50
65
55
15
30
60
40
0
-1 8
20
30
RR
15
-1 7
0
-2
RR
10
17
40
25
60
55
65
55
삭제의 경우, 삭제된 노드의 부모노드부터 루트까지 균형인수를 검토하게 되는데, 이 경로에 있는 노드들이 회전에 사용되는 B노드, C노드가 되지 않는다. (삽입과 차이점)
단말노드의 삭제 키 값이 5인 노드의 삭제로 균형을 맞추기 위해 두 번의 회전이 필요하다.
27/30 27/30
AVL 트리: 삭제 예 – 계속 -1
-1
15
15
-1
-1 7
-1
30
7
25 1
5
8
20
10
17
50
25
40
55
5
60
65
-1 20
8
10
65
17
50
40
55
60
65
자식이 둘인 중간 노드의 삭제 이 경우에는 전혀 회전이 필요없다.
28/30 28/30
AVL 트리: 삭제 예 – 계속 -2
-1 10
15 0
-1 7
10
25
17
5
50
40
55
10
1
-1 20
8
-1 7
25 1
5
25
60
-1 20
8
17
65
50
50
7
60
40
55
5
20
8
17
40
55
60
65
65
29/30 29/30
AVL 트리 분석 AVL 트리
이진 검색 트리
찾기: T(N) = O(log2N) 처리: T(N) = O(1) 삽입 균형: W(N) = O(log2N) 전체: W(N) = O(log2N)
전체: W(N) = O(N)
찾기: T(N) = O(log2N) 처리: W(N) = O(log2N) 삭제 균형: W(N) = O(log2N 전체: W(N) = O(log2N)
전체: W(N) = O(N)
검색 전체: T(N) = O(log2N)
전체: W(N) = O(N)
회전 비용은 상수 시간이며, 최악의 경우 트리 높이만큼 회전할 수도 있다. 그러나 이것의 전체 비용은 트리 높이에 비례한다. 즉, AVL 트리는 일반 이진 검색 트리와 달리 삽입과 삭제의 연산 비용도 저렴하면서 검색은 항상 O(log2N)인 트리이다. 30/30 30/30
©copyright 2005
자료구조 및 실습(INA240) NOTE 11
그래프
한국기술교육대학교 인터넷미디어공학부 김상진
교육목표 개요 그래프는 트리를 포함하고 있는 보다 큰 개념의 자료구조로서, 노드와 또 다른 노드 간에 여러 경로가 존재할 수 있고, 간선의 방향이 존재할 수 있다. 비가중치 그래프 그래프 탐색: 너비 우선, 깊이 우선 신장 트리 가중치 그래프 최단 경로 알고리즘
2/39
그래프 그래프(graph): 공집합이 아닌 정점(vertex)들의 유한 집합과 그래프 정점 이 정점들을 연결하는 간선(edge)으로 구성되는 자료구조 간선 수학적 정의: 그래프 G는 G = (V, E)로 정의되며, 여기서 V(G): 공집합이 아닌 정점들의 유한집합 E(G): 간선들의 집합 무방향 그래프(undirected graph): 간선의 방향성이 없는 그래프 그래프 유방향 그래프(directed graph, digraph): 간선의 방향성이 있는 그래프 그래프 방향 그래프에서는 간선을 아크(arc)라고도 한다. 아크 인접 정점(adjacent vertex): 간선에 의해 연결되어 있는 정점 정점 경로(path): 두 개의 정점을 연결하는 일련의 정점들 경로 정점 a에서 b까지의 경로 a, v1, v2, …, vn, b가 존재하기 위해서는 간선 (a, v1), (v1, v2), …, (vn-1, vn), (vn, b)가 존재해야 한다. 3/39
그래프 – 계속 M
A
V(G1)= {A,B,E,L,M}
E B
D
E(G1)= {(A,E),(B,L),(E,A) (L,L),(L,A),(M,B),(M,E)} B
C V(G1)={A,B,C,D} A
L
Path(M,A): M, B, L, A - 길이: 3
E(G1)={(A,B),(A,D),(B,C),(C,D)}
경로의 길이: 길이 경로를 구성하는 간선의 수 주기(cycle): 첫 번째 정점과 마지막 정점이 같은 경로 주기 정점의 차수: 차수 정점에 연결된 간선의 수를 말하며, 유방향 그래프에서는 진입 차수(indegree)와 진출 차수(outdegree)로 나누어 고려된다. 차수 차수 4/39
그래프 – 계속 완전 그래프(complete graph): 최대 수의 간선을 가진 그래프 그래프 모든 정점이 인접 정점이 된다. N개의 정점이 있으면 유방향은 N(N-1)개, 무방향은 N(N-1)/2개의 간선이 존재한다.
완전 유방향 그래프 (complete directed graph)
완전 무방향 그래프 (complete undirected graph)
5/39
가중치 그래프 가중치 그래프(weighted graph): 간선에 가중치가 할당되어 있는 그래프 그래프 인천
10
120
서울
120
원주
강릉
90 160 180
140
천안
250
50
대전
광주
200
60 60 80
대구
부산 80
6/39
비가중치 그래프의 표현 방법 1. 인접 행렬
M
A E B
D B
A
B
E
L
M
A
0
0
1
0
0
B
0
0
0
1
0
E
1
0
0
0
0
L
1
0
0
1
0
M
0
1
1
0
0
C A A
B
C
D
A
0
1
0
1
B
1
0
1
0
C
0
1
0
1
D
1
0
1
0
L
행의 합: 정점의 진출 차수 열의 합: 정점의 진입 차수
합: 정점의 차수
단점. n2 공간이 필요 - 무방향 행렬은 이것의 반만으로 표현가능
7/39
비가중치 그래프의 표현 – 계속 방법 2. 인접 리스트
M
A E B
D B
A
E
B
L
E
A
L
A
L
M
B
E
C A A
B
D
B
A
C
C
B
D
D
A
C
L
단점. 진입 차수를 계산하는 것이 어렵다. - 역인접 리스트 유지
8/39
Graph ADT
9/39
Graph ADT import java.util.Iterator; interface Graph{ int DEF_CAPACITY = 10; int DFS = 1; // 탐색 방법을 지정하기 위한 상수 int BFS = 2; // 탐색 방법을 지정하기 위한 상수 boolean isEmpty(); boolean isFull(); void clear(); void insertVertex(String label) throws GraphOverflowException; void removeVertex(String label) throws GraphUnderflowException; void removeEdge(String from, String to) throws GraphUnderflowException; // 비가중치 그래프 // void insertEdge(String from, String to); // 가중치 그래프 // void insertEdge(String from, String to, int weight); Iterator iterator(int type, String start); } 10/39 10/39
public abstract ListGraph implements Graph{ protected class Vertex{ public String label; public SortedLinkedList edges; } protected class GraphIterator implements Iterator{ … } protected Vertex[] graph; protected int size = 0; // 정점의 개수 인접 리스트 표현 방법을 사용 public ListGraph(){ setup(DEF_CAPACITY); } public ListGraph(int capacity){ if(capcity>0) setup(capacity); else setup(DEF_CAPACITY); } private void setup(int capacity){ … } public boolean isEmpty(){ return (size == 0); } public boolean isFull(){ return (size == graph.length); } public void clear() { … } protected int index(String label){ … } // 정점의 색인 찾기 public void insertVertex(String label) throws GraphOverflowException { … } public abstract void removeVertex(String label) throws GraphUnderflowException; public abstract void removeEdge(String from, String to) throws GraphUnderflowException; public boolean search(int type, String from, String to) throws GraphUnderflowException { … } public Iterator iterator(int type, String start) throws GraphUnderflowException { … } } 11/39 11/39
WeightedListGraph, UnweightedListGraph public abstract class WeightedListGraph extends ListGraph{ public WeightedListGraph(){ super(); } public WeightedListGraph(int capacity){ super(capacity); } public abstract void insertEdge(String from, String to, int weight); } public abstract class UnweightedListGraph extends ListGraph{ public WeightedListGraph(){ super(); } public WeightedListGraph(int capacity){ super(capacity); } public abstract void insertEdge(String from, String to); }
12/39 12/39
UndirectedUnweightedListGraph public abstract UndirectedUnweightedListGraph implements UnweightedGraph{ public UndirectedUnweightedListGraph(){ super(DEF_CAPACITY); } public UndirectedUnweightedListGraph(int capacity){ super(DEF_CAPACITY); } public void removeVertex(String label) throws GraphUnderflowException { … } public void removeEdge(String from, String to) throws GraphUnderflowException { … } public void insertEdge(String from, String to) throws GraphUnderflowException { … } }
13/39 13/39
Insert Vertex public void insertVertex(String label) throws GraphOverflowException{ if(label==null) throw new NullPointerException(“…”); if(isFull()) throw new GraphOverflowException(“…"); Vertex v = new Vertex(); v.label = label; v.edges = new SortedLinkedList(); graph[size] = v; size++; } // ListGraph: insertVertex protected int index(String label){ for(int i=0; i<size; i++){ if(label.equals(graph[i].label)) return i; } return -1; 정점들은 배열을 이용한 비정렬 방식으로 유지 } // ListGraph: index
14/39 14/39
Insert Edge public void insertEdge(String from, String to) throws GraphUnderflowException{ if(from==null||to==null) throw new NullPointerException(“…”); if(isEmpty()) throw new GraphUnderflowException(“…”); int v1 = index(from); int v2 = index(to); // v1 정점이 존재하지 않는 경우 if(v1==-1) throw new GraphUnderflowException(“…”); // v2 정점이 존재하지 않는 경우 if(v2==-1) throw new GraphUnderflowException(“…”); graph[v1].edges.insert(graph[v2].label); graph[v2].edges.insert(graph[v1].label); } // UndirectedUnweightedListGraph: insertEdge 무방향 그래프이므로 양쪽 인접리스트에 모두 추가해야 함.
15/39 15/39
Remove Vertex public void removeVertex(String label) throws GraphUnderflowException{ if(label==null) throw new NullPointerException(“…”); if(isEmpty()) throw new GraphUnderflowException(“…”); int v = index(label); // 제거하고자 하는 정점이 존재하지 않는 경우 if(v == -1) throw new GraphUnderflowException(“…”); graph[v] = graph[size-1]; size--; for(int i=0;i<size;i++){ if(!graph[i].edges.isEmpty()) graph[i].edges.delete(label); } } // UndirectedUnweightedListGraph: RemoveVertex 정점을 제거하는 이 정점에 연결된 모든 간선을 제거해야 한다.
16/39 16/39
Remove Edge public void removeEdge(String from, String to) throws GraphUnderflowException{ if(isEmpty()) throw new GraphUnderflowException(“…”); if(from==null||to==null) throw new NullPointerException(“…”); int v1 = index(from); int v2 = index(to); // v1 정점이 존재하지 않는 경우 if(v1==-1) throw new GraphUnderflowException(“…”); // v2 정점이 존재하지 않는 경우 if(v2==-1) throw new GraphUnderflowException(“…”); // v1에서 v2를 잇는 간선이 존재하지 않는 경우 if(!graph[v1].edges.delete(graph[v2].label)) throw new GraphUnderflowException(“…”); graph[v2].edges.delete(graph[v1].label); } // UndirectedUnweightedListGraph: RemoveEdge 무방향 그래프이므로 양쪽 인접리스트에 모두 제거해야 함.
17/39 17/39
Traverse 깊이우선탐색(DFS, Depth First Search) 깊이우선탐색 단계 1. 정점 i를 방문한다. 단계 2. 정점 i에 인접한 정점 중에 아직 방문하지 않은 정점이 있으면 모두 스택에 저장한다. 단계 3. 스택에서 정점을 제거하고, 이 정점을 i로 하여 단계 1부터 다시 수행한다. 만약 스택이 비어있거나 모든 정점을 방문하였으면 종료한다. A A 순회 순서: A,D,G,C,F,B,E C
순회 순서: A,C,E,D,B
C D
B
B G
D
E
E
F
18/39 18/39
Traverse 너비우선탐색(BFS, Breadth First Search) 너비우선탐색 단계 1. 정점 i를 방문한다. 단계 2. 정점 i에 인접한 정점 중에 아직 방문하지 않은 정점이 있으면 모두 큐에 저장한다. 단계 3. 큐에서 정점을 제거하고, 이 정점을 i로 하여 단계 1부터 수행한다. 만약 큐가 비어있거나 모든 정점을 방문하였으면 종료 한다. A A 순회 순서: A,B,C,D,F,E,G C
순회 순서: A,B,C,E,D
C D
B
B G
D
E
E
F
19/39 19/39
ListGraph의 반복자 클래스 public Iterator iterator(int type, String start) throws GraphUnderflowException { if(start==null) throw new NullPointerException(“…”); int v = index(start); if(v==-1) throw new GraphUnderflowException(“…”); if(type==Graph.BFS||type==Graph.DFS) return new GraphIterator(type, v); else throw new GraphUnderflowException(“…”); } protected class GraphIterator implements Iterator{ LinkedQueue traverseQueue; public GraphIterator(int type, int start){ … } public boolean hasNext() { … } public Object next() { … } public void remove() { … } private void BreadthFirstSearch(int start); private void DepthFirstSearch(int start); } public GraphIterator(int type, int start){ traverseQueue = new LinkedQueue(); if(type==Graph.BFS) BreadthFirstSearch(start); else DepthFirstSearch(start); } 20/39 20/39
연결 여부 연결 그래프(connected graph): 무방향 그래프에서 서로 다른 모든 그래프 쌍의 정점들 사이에 경로가 존재하는 그래프 DFS, BFS를 이용하여 연결 여부를 확인할 수 있다. DFS(G, i): i 노드로부터 시작하여 방문한 모든 정점 이 때 G = DFS(G, i)이면 연결 그래프 어떤 연결 그래프의 모든 정점의 차수가 짝수이면 여기서 하나의 간선을 제거하여도 여전히 연결 그래프이다.
21/39 21/39
Spanning Tree 부분 그래프(subgraph): 다음이 성립하면 G'(V', E')는 G(V, E)의 그래프 부분 그래프라 한다. V' ⊆ V, E' ⊆ E 신장 트리(spanning tree): G의 부분 그래프 중 G의 모든 정점들을 트리 포함하는 트리 A
A
C
B
C
D
E
D
A
B
A
C
B
C
B
D
E
D
E
부분 그래프이지만 최소 신장트리가 신장 트리가 아님 아님
최소 신장트리
22/39 22/39
최소 신장 트리 최소 신장 트리(minimum spanning tree): 최소의 간선 수로 트리 구성된 그래프의 신장 트리 그래프의 정점 수가 N이면 최소 신장 트리의 간선 수는 N-1이다. DFS, BFS로 만들어진 신장 트리는 최소 신장 트리가 된다. 깊이우선 신장트리(depth first spanning tree) 신장트리 너비우선 신장트리(breadth first spanning tree) 신장트리 A
A
A
C
B
C
B
C
B
D
E
D
E
D
E
원 그래프
BFS(A)의 신장 트리
DFS(A)의 신장 트리
23/39 23/39
가중치 그래프의 표현 방법 1. 인접 리스트 서울
동경
부산
100
북경
60
180
부산 제주
북경
180
동경 홍콩
60
600
시애틀
1600 1200 100
120
홍콩
시애틀 -
800 서울
북경 하와이
화와이
90
60 부산
40
동경 20
제주
40
24/39 24/39
가중치 그래프의 표현 – 계속 방법 2. 인접 행렬 0
1
2
3
4
5
6
7
8
9
0
서울
0
0
60
90
100
120
180
800
1600
-
-
1
부산
1
60
0
-1
20
-1
-1
-1
-1
-
-
2
제주
2
90
-1
0
40
40
-1
-1
-1
-
-
3
동경
3
100
-1
-1
0
-1
-1
-1
1200
-
-
4
홍콩
4
-1
-1
-1
-1
0
60
-1
-1
-
-
5
북경
5
180
-1
-1
-1
-1
0
-1
-1
-
-
6
하와이
6
-1
-1
-1
-1
-1
-1
0
600
-
-
7
시애틀
7
1600
-1
-1
1200
-1
-1
600
0
-
-
8
-
8
-
-
-
-
-
-
-
-
-
-
9
-
9
-
-
-
-
-
-
-
-
-
-
25/39 25/39
public abstract MatrixGraph implements Graph{ public static final int NULLEDGE = -1; protected class GraphIterator implements Iterator{ … private void setup(int capacity){ } graph = new String[capacity]; protected String[] graph; adjMatrix = new int[capacity][capacity]; protected int[][] adjMatrix; for(int i=0; i<capacity; i++) protected int size = 0; // 정점의 개수 for(int j=0; j<capacity; j++) public MatrixGraph(){ if(i!=j) adjMatrix[i][j] = NULLEDGE; setup(DEF_CAPACITY); else adjMatrix[i][j] = 0; } } public MatrixGraph(int capacity){ if(capcity>0) setup(capacity); else setup(DEF_CAPACITY); } 인접 행렬 표현 방법을 사용 private void setup(int capacity){ … } public boolean isEmpty(){ return (size == 0); } public boolean isFull(){ return (size == graph.length); } public void clear() { … } protected int index(String label){ … } // 정점의 색인 찾기 public void insertVertex(String label) throws GraphOverflowException { … } public abstract void removeVertex(String label) throws GraphUnderflowException; public abstract void removeEdge(String from, String to) throws GraphUnderflowException; public boolean search(int type, String from, String to) throws GraphUnderflowException { … } public Iterator iterator(int type, String start) throws GraphUnderflowException { … } } 26/39 26/39
WeightedMatrixGraph, UnweightedMatrixGraph public abstract class WeightedMatrixGraph extends MatrixGraph{ public WeightedMatrixGraph(){ super(); } public WeightedMatrixGraph(int capacity){ super(capacity); } public abstract void insertEdge(String from, String to, int weight); } public abstract class UnweightedMatrixGraph extends MatrixGraph{ public WeightedMatrixGraph(){ super(); } public WeightedMatrixGraph(int capacity){ super(capacity); } public abstract void insertEdge(String from, String to); }
27/39 27/39
DirectedWeightedMatrixGraph public class DirectedWeightedMatrixGraph implements WeightedListGraph{ public DirectedWeightedMatrixGraph(){ super(); } public DirectedWeightedMatrixGraph(int capacity){ super(capacity); } public void removeVertex(String label) throws GraphUnderflowException { … } public void removeEdge(String from, String to) throws GraphUnderflowException { … } public void insertEdge(String from, String to, int weight) throws GraphUnderflowException { … } }
28/39 28/39
Insert Vertex public void insertVertex(String label) throws GraphOverflowException{ if(label==null) throw new NullPointerException(“…”); if(isFull()) throw new GraphOverflowException(“…”); graph[size] = label; size++; } // MatrixGraph: insertVertex
29/39 29/39
Insert Edge public void insertEdge(String from, String to, int weight) throws GraphUnderflowException{ if(from==null||to==null) throw new NullPointerException(“…”); if(isEmpty()) throw new GraphUnderflowException(“…”); int v1 = index(from); int v2 = index(to); if(v1==-1) throw new GraphUnderflowException(“…”); if(v2==-1) throw new GraphUnderflowException(“…”); adjMatrix[v1][v2] = weight; } // DirectedWeightedMatrixGraph: insertEdge
30/39 30/39
Remove Vertex public void removeVertex(String label) throws GraphUnderflowException{ if(label==null) throw new NullPointerException(“…”); if(isEmpty()) throw new GraphUnderflowException(“…”); int v = index(label); if(v == -1) throw new GraphUnderflowException(“…”); size--; graph[v] = graph[size]; for(int i=0;i<size; i++){ adjMatrix[v][i] = adjMatrix[size][i]; adjMatrix[size][i] = NULLEDGE; adjMatrix[i][v] = adjMatrix[i][size]; adjMatrix[i][size] = NULLEDGE; } adjMatrix[v][v] = NULLEDGE; }// DirectedWeightedMatrixGraph: insertEdge
31/39 31/39
최단 경로 알고리즘 최단 경로 문제: 문제 정점 간에 가장 짧은 경로를 찾는 문제 이 문제는 그래프의 종류에 따라 다양한 응용 문제가 존재한다. 그래프가 유한 그래프인지 무한 그래프인지? 유한 그래프: 노드의 수가 유한한 그래프 그래프가 유방향인지 무방향 그래프인지? 간선의 가중치가 모두 같은 경우, 가중치가 모두 양수인 경우, 음수의 가중치가 있는 경우(가장 어려움)
32/39 32/39
최단 경로 알고리즘 1. 모든 간선의 가중치가 같은 유한 무방향 그래프에서 정점 s에서 정점 t까지 길이가 가장 짧은 경로를 찾아라. BFS 검색을 이용하면 찾을 수 있다. Moore 알고리즘: 알고리즘 정점의 수는 n이라 하자. 단계 1. ∀λ[v] = -1 λ[s] = 0; 단계 2. l = 0; 단계 3. λ[v] = l인 정점 v와 인접한 모든 정점 u 중 λ[u]가 -1이면 λ[u] = l +1, 인접한 노드가 없으면 종료한다. 단계 4. λ[t] != -1이면 종료한다. 단계 5. l = l +1; 단계 3부터 반복
33/39 33/39
최단 경로 알고리즘 1. – 계속 A에서 D까지
A
B
E
C
D
C에서 A까지
A
B
C
D
E
A
B
C
D
E
-1
-1
-1
-1
-1
-1
-1
-1
-1
-1
A
B
C
D
E
A
B
C
D
E
0
-1
-1
-1
-1
-1
-1
0
-1
-1
A
B
C
D
E
A
B
C
D
E
0
1
-1
-1
1
-1
1
0
1
1
A
B
C
D
E
A
B
C
D
E
0
1
2
2
1
2
1
0
1
1
34/39 34/39
최단 경로 알고리즘 2. 모든 간선의 가중치가 양수인 유한 유방향 그래프에서 정점 s에서 정점 t까지 길이가 가장 짧은 경로를 찾아라. Dijkstra 알고리즘 정점의 수는 n이라 하고, 정점들의 집합을 V라 하자. 단계 1. ∀λ[v] = ∞; λ[s] = 0; 단계 2. T Å V 단계 3. T 중에 λ[u]가 최소인 정점을 u라 하자. 단계 4. u가 t이면 종료 e → v 에 대해 v∈T이고 단계 5. 정점 u에서 진출하는 모든 간선 u ⎯⎯ λ[v] > λ[u]+l(e)이면 λ[v] = λ[u]+l(e) 단계 6. T Å T - {u}, 단계 3부터 반복
35/39 35/39
최단 경로 알고리즘 2. – 계속 A에서 D까지의 최단 경로의 예
A
5
2
A
B
C
D
E
E
0
99
99
99
99
1
A
B
C
D
E
0
2
3
99
5
A
B
C
D
E
0
2
3
8
5
A
B
C
D
E
0
2
3
8
4
A
B
C
D
E
0
2
3
6
4
3 B 7 6
T = {B,C,D,E}
C
3 D
T = {A,B,C,D,E}
2
T = {C,D,E}
T = {D,E}
T = {D}
36/39 36/39
최단 경로 알고리즘 3. A
간선의 가중치가 음수가 될 수 있는 유한 유방향 그래프에서 정점 s에서 정점 t까지 길이가 가장 짧은 경로를 찾아라. 음의 주기가 존재하지 않음 n이 정점의 수일 때, 음의 주기가 존재하지 않으면 최단 경로의 길이는 최대 n-1이다. 기본 생각 길이가 1인 것부터 n-1인 것까지 모두 구한다.
5 3 B -2 C
A
5 3 B -2 1 C
음의 주기가 존재하는 경우
37/39 37/39
최단 경로 알고리즘 3. – 계속 D
5 -1
3
A
B
C
D
E
A
B
C
D
E
0
99
99
99
99
99
99
99
0
99
A
B
C
D
E
A
B
C
D
E
0
5
3
-2
99
99
99
99
0
7
3
B -2 C
경로의 길이=3
경로의 길이=1
-2 A
E -1
경로의 길이=2
경로의 길이=4
A
B
C
D
E
A
B
C
D
E
99
99
99
4
99
99
99
99
99
3
A
B
C
D
E
99
1
99
4
2
A
B
C
D
E
99
1
99
4
1
38/39 38/39
최단 경로 알고리즘 3. – 계속 Bellman과 Ford 알고리즘 Distk[u]: 시작 정점 v에서 u까지 최대 k개의 아크를 포함할 수 있는 최단 경로 Dist k [ u] ← min(Dist k −1[ u],min(Dist k −1[ i ] + weight[i , u])) A
D
5 -1
3
3
B -2 C
E -1
Dist1
A
B
C
D
E
A
0
5
3
-2
99
A
B
C
D
E
B
99
0
99
-1
99
0
5
3
-2
99
C
99
-2
99
99
-1
D
99
99
99
99
3
E
99
99
99
99
99
-2
Dist2 각 정점의 진입차수 고려(열) Dist2[B] = min(5, (3+(-2))=1 Dist2[D] = min(-2, (5+(-1))=-2 Dist2[E] = min(99, min(3+(-1), -2+3))=1
39/39 39/39