상세 컨텐츠

본문 제목

[JAVA] 자바 스터디 4주차 - 제어문

개발 공부 (etc)/JAVA

by letprogramming 2021. 2. 1. 15:52

본문

반응형

목표


자바가 제공하는 제어문 학습

 

학습할 것


  • 선택문
  • 반복문

 

제어문


제어문이란 프로그램의 흐름을 제어할 수 있도록 도와주는 실행문이다.

즉, 제어문을 이용하면 프로그램이 흘러가는 순서를 프로그래머가 원하는 대로 흐르도록 제어할 수 있다는 의미이다.

자바의 제어문에는 선택문과 반복문이 있다.

 

선택문


선택문은 변수에 따라 실행문을 선택하는 제어문이다.

앞에서 공부했었던 switch - case 구문을 예로 들 수 있다.

 

switch (num) {
	case 0: System.out.println("영");
    break;
    	case 1: System.out.println("일");
    break;
    	case 2: System.out.println("이");
    break;
    	case 3: System.out.println("삼");
    break;
    	case 4: System.out.println("사");
    break;
    	case 5: System.out.println("오");
    break;
    	case 6: System.out.println("육");
    break;
}

위의 예시에서 num이라는 변수가 어떤 값이냐에 따라 실행할 구문이 선택된다.

 

만약 num이 4라면 case 뒤에 있는 값 중에서 4와 일치되는 System.out.println("사") 가 실행될 것이다.

 

break

switch문에서는 case내에서 break를 사용하지 않으면 다음 case가 그대로 실행된다.

지난번에 java13과 switch문에 대해 공부하면서 개선된 switch문에 대해 공부했었다.

 

java12에서는 람다식을 이용할 수 있었는데 화살표(->)를 이용하면 break를 명시하지 않아도 다음 케이스로 넘어가지 않도록 개선되었다.

 

default

switch문 내의 default는 예외 처리와 유사한 성격이다.

만약 case로 정의되지 않은 값이 들어올 경우 default에 명시된 코드가 실행된다.

만약 default를 명시하지 않는다면 에러가 발생할 수 있기 때문에 default는 명시하는 것이 좋다.

 

반복문


반복문은 실행문을 반복해서 실행하는 것을 의미한다.

자바에서 반복문의 종류는 for와 while, do while 이 있다.

 

for 문

for(int i = 0; i < 10; i++){
	//실행문
}

for문의 기본적인 형태는 위와 같다.

int i = 0은 초기값으로 반복문을 수행하기 위한 변수를 선언 및 초기화할 수 있다.

 

가운데 i < 10은 조건문이며 이 조건문의 결과가 true가 될 때만 반복문 내부의 실행 코드를 수행한다.

i < 10 은 i 라는 변수가 10보다 작다면 반복문 내의 실행문을 실행하라는 의미이다.

 

마지막 i++ 는 반복문이 한 번 끝날 때마다 수행되는 구문이다.

반복문 내의 모든 실행문이 끝나고 나면 i 변수에 1을 더하고 다시 for문의 조건문을 살피는 식으로 반복한다.

 

while 문

int i = 0;
while(i < 10){
	//실행문
    i++;
}

while문은 반복문에 사용하는 변수를 직접 선언하고 증감시켜주어야 한다.

반복문이 실행되기 전에 while문의 괄호에 있는 조건을 만족하는지 확인하고

조건이 참이라면 반복해서 실행한다.

 

do while문

int i = 0;
do {
	//실행문
} while (i < 10);

do while문은 while문과 유사하지만 실행 순서에 차이점이 있다.

while문은 최초에 조건식이 참인지 거짓인지를 판단하여 실행할지 말지를 결정한다.

반면에 do while문의 경우 최초 1회는 무조건 실행을 한 이후에 조건식을 살핀다.

 

만약 while문의 조건이 false이더라도, do에 작성되어 있는 코드를 한 번은 실행하고 종료되는 것이다.

 

break

위의 반복문들 내에서 break를 작성하면 바로 반복문을 탈출한다.

현재 반복문 조건에 상관없이 강제 종료라고 생각하면 된다.

보통 특정한 조건에서 반복문을 종료해야 하거나 더 이상 진행할 필요가 없을 때 사용한다.

 

continue

continue문은 남은 코드를 무시한다고 생각하면 된다.

for(int i = 0 ;i < 10; i++){
	if(i > 5) continue;
    System.out.println("Hello");
}

예를 들어 위와 같은 코드가 있다고 생각했을 때,

출력값인 Hello는 6번밖에 나오지 않는다.

 

그 이유는 i가 0에서 5까지 증가하면서 "Hello"를 출력하고

i가 6이 되는 순간부터 continue문이 실행되기 때문이다.

 

반복문이 실행되다가 continue문을 만나면 남은 코드를 무시하고 다음 반복 턴으로 넘어간다.

break문과 차이점은

break문은 무조건 반복문 자체를 끝내고 더 이상 반복문을 실행하지 않는다.

continue문은 반복문을 끝내는 것이 아니라, 현재 턴만 그대로 지나가는 것이다.

 

for each

자바의 for each는 J2SE 5.0부터 추가되었다.

for each문의 사용법은 for문과 유사하다. each라는 다른 키워드가 존재하는 것이 아니라 for를 그대로 사용한다.

 

for each문은 기존의 for문과 다르게 조건을 구성한다.

for (자료형 변수이름:iterate 객체){

}

위와 같이 콜론(:)을 사이에 두고 두 가지만 명시하면 된다.

먼저 오른쪽의 iterate 객체는 반복해서 접근하는 객체라고 생각하면 된다.

리스트, 배열 등과 같이 반복문이 접근해서 데이터를 받아올 수 있는 객체이다.

 

왼쪽의 변수는 오른쪽의 객체에 접근해서 받아오는 데이터를 하나씩 대입할 수 있는 변수이다.

 

String[] names = {"Kim", "Lee", "Park"};
for(String name : names){
	System.out.println(name)
}

예를 들어 위의 코드에서

iterate객체는 names이다. String 배열인 names를 콜론 오르쪽에 명시하면서 원소에 하나씩 접근한다.

이 원소들을 사용하기 위해서는 변수가 필요한데 이 때 받는 변수가

왼쪽에 String 타입으로 선언된 name이라는 변수이다.

반복문을 한 번 돌때마다 names의 원소들에 하나씩 접근하면서 name변수에 대입한다.

 

일반적인 for문과 달리 객체의 원소에 직접 접근하고 객체를 바로 사용할 수 있어서

인덱스로 접근할 필요가 없어 편리하고,

반복문을 위한 별도의 인덱스 변수도 필요하지 않아서 코드가 직관적이고 간결하다.

 

과제 0. JUnit 5 학습하기


  • 인텔리J, 이클립스, VS Code에서 JUnit 5로 테스트 코드 작성하는 방법에 익숙해 질 것
  • 이미 JUnit 알고 계신분들은 다른 것 아무거나!
  • 더 자바, 테스트 강의도 있으니 참고하세요~

JUnit

JUnit 5에 대해 알아보기 전에 JUnit 자체에 대해서 공부해본다.

JUnit이란 자바 용 유닛(단위) 테스트 프레임워크이다.

 

단위 테스트란 개발을 진행하면서 특정 모듈이 정상적으로 의도한대로 작동하는지 테스트하는 것을 의미한다.

프로젝트를 모두 완성한 후에 테스트를 하면 오류의 위치 확인도 어렵고 디버깅에 많은 시간과 비용이 소비될 수 있다.

 

단위 테스트를 수행한다는 것은 수동으로 테스트 케이스를 만들고 하나씩 실행해보면서 제대로 작동하는지 확인하는 것이 아니라 

이 과정이 자동화되었을 때 효과적이다.

 

자바 언어로 프로그램을 개발할 때 이런 자동화된 단위테스트를 수행하기 위해서 나온 프레임워크가 JUnit이라고 할 수 있다.

 

JUnit5는 기존의 JUnit들과 다르게

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

이렇게 구성되어 있다.

 

- JUnit PlatformJVM에서 테스트 프레임워크를 시작하기 위한 기초적인 역할을 수행하고 API를 제공한다. 

- JUnit Jupiter는 JUnit5에서 테스트 및 Extension을 작성하기 위한 새로운 프로그래밍 모델과 확장 모델의 조합이며, TestEngine을 제공한다. jupiter-api를 통해서 테스트 코드를 작성할 수 있고, jupiter-engine이 이 코드를 발견하고 실행한다.

- JUnit Vintage는 하위 호환성을 위한 TestEngine으로 이전 버전들인 JUnit4와 JUnit3를 실행할 수 있다.

 

테스트 작성과 테스트 실행을 위한 API가 분리되었기 때문에 사용자는 필요한 API만 불러와서 사용할 수 있다.

 

JUnit 5를 사용하기 위해서는 일단 라이브러리를 추가해야 한다.

이 때 필요한 것이 Maven이나 Gradle 같은 빌드 자동화 도구이다.

라이브러리를 수동이 아닌 자동으로 관리하고 동기화하기 위해서 사용한다.

 

Gradle을 이용해 자바 프로젝트를 생성해서 JUnit5를 사용했다.

1. 프로젝트 폴더 생성 (mkdir 프로젝트명)

2. brew install gradle

3. gradle init

4. gradle clean build

5. gradle run

 

- gradle로 자바 애플리케이션 프로젝트를 생성했을 때, 자바 패키지 이름이 java로 시작해서 오류가 발생했었다. (java.study.App)

com.study.App 으로 수정한 이후에 정상적으로 수행되는 것을 확인할 수 있었다.

 

Jupiter API : Annotation

@BeforeAll : @Test 메소드들이 실행되기 전에 실행된다.

@BeforeEach : 각각의 @Test 메소드가 실행되기 전에 실행된다.

@Test

@AfterEach : 각각의 @Test 메소드가 실행된 후에 실행된다.

@AfterAll : @Test 메소드들이 실행된 후에 실행

@DisplayName : 테스트 클래스나 메소드의 표시 이름을 지정

@Nested : 계층 구조로 테스트 가능

@ParameterizedTest : 테스트 데이터를 매개변수 형태로 사용 가능

@Disabled : 테스트를 실행 대상에서 제외(생략)

 

package com.study;

import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.*;

class AppTest5 {
    @BeforeAll
    static void init() {
        System.out.println("Before All");
    }

    @BeforeEach
    void setUp() {
        System.out.println("Before Each");
    }

    @AfterEach
    void tearDown() {
        System.out.println("After Each");
    }

    @AfterAll
    static void finish() {
        System.out.println("After All");
    }

    @Test
    @DisplayName("Greeting 테스트")
    void getGreeting() {
        App app = new App();
        System.out.println("Greeting Test");
        assertNotNull(app.getGreeting(), "Greeting Need");
    }

    @Test
    void main() {
        App app = new App();
        System.out.println("Main Test");
        assertNotNull(app, "App Need");
    }
}

위의 테스트를 수행했을 때

테스트 결과

테스트의 이름도 @DisplayName에서 지정한대로 바뀐 것을 확인할 수 있고,

Annotation을 지정한대로 출력이 되는 것을 확인할 수 있다.

Jupiter API : Assert

Assertions 클래스는 값 검증을 위한 static 메소드들을 제공한다.

  • assertEquals(expected, actual) : int, long 등 기본타입과 Object에 대한 assertEquals 메소드가 존재한다.
  • assertNotEquals(Object unexpected, Object actual)
  • assertTrue(boolean condition)
  • assertFalse(boolean condition)
  • assertNull(Object actual)
  • assertNotNull(Object actual)
  • assertAll() : 여러 개의 assert를 실행하여 결과를 확인 (실패하더라도 모든 결과 확인 지원)
  • assertThrows() : 예외 검증 가능
  • assertTimeout() : 테스트 실행 시간 검증 가능
  • fail()

간단한 곱셈 예제를 수행해본다.

<Multiply.java>

package com.study;

public class Multiply {
    private int x;
    private int y;

    public Multiply(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public Multiply() {
        this.x = 1;
        this.y = 1;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int multi(int x, int y){
        return x * y;
    }
}

<MultiplyTest.java>

package com.study;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MultiplyTest {
    @Test
    @DisplayName("테스트1 4 X 6 = 24")
    void multi1() {
        Multiply multiply = new Multiply(4, 6);
        assertEquals(multiply.multi(multiply.getX(), multiply.getY()), 24);
    }
    @Test
    @DisplayName("테스트2 4 X 7 = 28")
    void multi2() {
        Multiply multiply = new Multiply(4, 7);
        assertEquals(multiply.multi(multiply.getX(), multiply.getY()), 24);
    }
}

테스트 파일을 만들때는 테스트하고 싶은 클래스에서 Ctrl + Enter를 누르면 Test 코드를 쉽게 작성할 수 있다.

테스트1은 4 X 6의 결과와 24가 같으므로 통과되었지만

테스트2는 4 X 7 의 결과가 예상했던 24가 아닌 28이 나왔으므로 통과하지 못하고 에러가 발생했다.

테스트 결과 (테스트1 성공, 테스트2 실패)

 

과제 1. live-study 대시 보드를 만드는 코드를 작성하세요.


  • 깃헙 이슈 1번부터 18번까지 댓글을 순회하며 댓글을 남긴 사용자를 체크 할 것.
  • 참여율을 계산하세요. 총 18회에 중에 몇 %를 참여했는지 소숫점 두자리가지 보여줄 것.
  • Github 자바 라이브러리를 사용하면 편리합니다.
  • 깃헙 API를 익명으로 호출하는데 제한이 있기 때문에 본인의 깃헙 프로젝트에 이슈를 만들고 테스트를 하시면 더 자주 테스트할 수 있습니다.

Github API가 있다는 것을 새롭게 알게되었다.

일단 자바 프로젝트에 Github API 라이브러리를 추가해야했다.

 

build.gradle 파일에

dependencies{
	compile 'org.kohsuke:github-api:1.119'
}

이렇게 작성해서 Github API 라이브러리의 의존성을 추가한다.

 

이미 스터디에 상세하게 작성해주신 분들의 코드를 참고해서

필요한 메소드나 데이터들을 빠르게 공부할 수 있었다.

 

먼저 Github에 있는 나의 계정과 연동을 해서 계정에 있는 레포지토리를 가져와야 한다.

계정을 연동하는 방법은 ID와 패스워드를 이용하는 방법개인 토큰을 이용하는 방법이 존재한다.

API 문서에는 개인 토큰을 이용하는 방법을 추천하고 있다.

아마 개인 정보인 아이디와 비밀번호가 코드에 직접적으로 들어가는 것이 보안 상 취약한 문제이기 때문에

not recommended 라고 써놓은 것 같다.

 

이후에 레포지토리의 이름을 넘겨서 getRepository()를 하면 레포지토리를 담고 있는 GHRepository 객체를 얻을 수 있다.

레포지토리 객체로부터 getIssues()를 통해 Issue 리스트를 받는다.

이 Issue리스트에서 Issue를 하나씩 빼서 그 안의 Comment 리스트를 또 받는다.

이 Comment의 작성자의 이름을 체크하면서 총 Issue에서 이 이름이 몇 번 나왔는지를 카운트하여 참여율을 계산한다.

주의할 점은 한 Issue에서 동일한 이름의 작성자가 Comment를 여러 번 남겼을 수도 있으므로

하나의 Issue내에서 하나의 이름은 한 번 카운팅해야한다.

 

다른 분들의 코드를 참고했기 때문에 유사한 코드가 많지만 자바 프로젝트에서 GIthub API를 사용해보는

새로운 경험을 해서 유익했다.

 

[소스 코드]

package com.study.week4;

import org.kohsuke.github.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

public class StudyDashboard {
    private static final String PERSONAL_KEY = "5564433fdea471e917cdff2327c68ffbf2164a1a";
    private GitHub github;
    private GHRepository repo;
    private List<GHIssue> issues;
    private List<GHIssueComment> comments;
    private HashSet<String> nameSet;
    private HashMap<String, Integer> commentMap;

    public StudyDashboard() throws IOException {
        github = new GitHubBuilder().withOAuthToken(PERSONAL_KEY).build();
        repo = github.getRepository("eogh234/live-study").getSource();
        nameSet = new HashSet<>();
        commentMap = new HashMap<>();

        issues = repo.getIssues(GHIssueState.ALL);
        for (GHIssue issue:issues) {
            comments = issue.getComments();
            nameSet = new HashSet<>();
            for (GHIssueComment comment : comments) {
                String name = comment.getUser().getName();
                if(name != null && !nameSet.contains(name)){
                    nameSet.add(name);
                    commentMap.put(name, commentMap.getOrDefault(name, 0) + 1);
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        StudyDashboard dashboard = new StudyDashboard();
        dashboard.commentMap.forEach((name, count) -> System.out.printf("%s - %.2f%%\n", name, (count / (float)dashboard.issues.size() * 100)));
    }
}

과제 2. LinkedList를 구현하세요.


  • LinkedList에 대해 공부하세요.
  • 정수를 저장하는 ListNode 클래스를 구현하세요.
  • ListNode add(ListNode head, ListNode nodeToAdd, int position)를 구현하세요.
  • ListNode remove(ListNode head, int positionToRemove)를 구현하세요.
  • boolean contains(ListNode head, ListNode nodeTocheck)를 구현하세요.

[소스코드]

package com.study.week4;

public class ListNode {
    private int data;
    private ListNode next;

    public ListNode(int data) {
        this.data = data;
        this.next = null;
    }

    public ListNode() {
        this.data = 0;
        this.next = null;
    }

    public ListNode add(ListNode head, ListNode nodeToAdd, int position) {
        if (head.next == null){
            head.next = nodeToAdd;
            nodeToAdd.next = null;
            return head;
        }
        for (ListNode node = head; node.next != null; node = node.next) {
            if (position <= 0) {
                nodeToAdd.next = node.next;
                node.next = nodeToAdd;
                break;
            } else {
                position--;
            }
        }

        return head;
    }

    public ListNode remove(ListNode head, int positionToRemove) {
        ListNode removedNode = new ListNode();
        ListNode prevNode = new ListNode();
        if (positionToRemove < 0){
            System.out.println("인덱스는 0이상\n");
            return null;
        }
        prevNode = head;
        removedNode = head.next;
        while (positionToRemove-- > 0){
            prevNode = removedNode;
            removedNode = removedNode.next;
        }

        prevNode.next = removedNode.next;

        return removedNode;
    }

    public boolean contains(ListNode head, ListNode nodeToCheck) {
        for (ListNode node = head; node != null; node = node.next) {
            if (node.data == nodeToCheck.data){
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        ListNode linkedList = new ListNode();
        ListNode node1 = new ListNode(10);
        ListNode node2 = new ListNode(20);
        ListNode node3 = new ListNode(30);
        linkedList.add(linkedList, node1, 0);
        linkedList.add(linkedList, node2, 0);
        linkedList.add(linkedList, node3, 0);

        for (ListNode node = linkedList.next; node != null; node = node.next){
            System.out.println(node.data);
        }

        System.out.printf("------------------------------\n");

        linkedList.remove(linkedList, 0);
        for (ListNode node = linkedList.next; node != null; node = node.next){
            System.out.println(node.data);
        }

        System.out.println(linkedList.contains(linkedList, new ListNode(10)));
    }
}

data와 next로 이루어진 연결리스트를 구현했다.

next에는 연결되어 있는 다음 노드의 객체가 저장되어 있다. 확실하게는 객체의 주소값이다.

 

add() : 삽입

삽입은 인자로 들어온 리스트의 head부터 노드를 하나씩 방문하면서 정해진 위치가 되면 삽입한다.

 

remove() : 삭제

삭제는 이전 노드를 저장하면서 연결리스트를 순회하다가 정해진 위치가 되면 이전 노드와 다음 노드를 연결해서 삭제한다.

 

contains() : 값 존재 확인

contains() 함수에서는 리스트를 순회하면서 주어진 노드의 데이터와 같은 값을 가지는 노드가 존재하면 true를 반환하고 리스트 끝까지 동일한 데이터를 가진 노드가 없다면 false를 반환한다.

 

과제 3. Stack을 구현하세요.


  • int 배열을 사용해서 정수를 저장하는 Stack을 구현하세요.
  • void push(int data)를 구현하세요.
  • int pop()을 구현하세요.
package com.study.week4;

public class Stack {
    private int stack[];
    private int top;

    public Stack() {
        stack = null;
        top = 0;
    }

    public void push(int data){
        top++;
        int temp[] = new int[top];
        for (int i = 0; i < top - 1; i++){
            temp[i] = stack[i];
        }
        stack = temp;
        stack[top - 1] = data;
    }

    public int pop(){
        if (top < 1){
            System.out.println("Empty Stack");
            return 0;
        }
        int removedNum = stack[top - 1];
        top--;
        if (top == 0){
            stack = null;
            return removedNum;
        }
        int temp[] = new int[top];
        for (int i = 0; i < top; i++){
            temp[i] = stack[i];
        }

        stack = temp;
        return removedNum;
    }

    public static void main(String[] args) {
        Stack stack = new Stack();
        stack.push(1);
        stack.push(2);
        stack.push(32);
        stack.push(41);
        stack.push(5);
        stack.push(500);

        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
    }
}

정수를 데이터로 담는 스택을 정수형 배열로 구현했다.

Stack 클래스에 저장해야 할 데이터는

실제 값들을 저장할 정수 배열

현재 스택의 가장 높은 위치(마지막에 들어온 데이터의 위치)를 가지는 top 인덱스이다.

push() : 삽입

스택에서 push를 할 때 가장 먼저 할 일은 현재 스택의 메모리를 증가시키는 것이다.

예를 들어, 원래 10개의 데이터를 담고 있던 스택이라면 새로운 데이터를 담을 공간이 필요하기 때문에

11개의 공간으로 늘려주어야 한다.

 

int 배열로 구현하기 위해서 push를 수행할 때 일단 스택의 top 값을 1 증가시킨다.

이후에 증가시킨 top만큼의 공간을 가지는 임시 배열을 선언한다.

 

이 배열에 기존의 스택의 데이터들을 넣고 난 후에 새로운 데이터를 마지막에 추가한다.

pop() : 제거

스택에서 pop을 할 때는 push와 반대로 top 값을 1 감소시킨다.

예를 들어, 10개의 데이터를 가지는 스택에서 pop이 수행되면 9개의 데이터만 남는다.

이 때, pop 이후의 top을 알고있어야 이후에 push나 pop 등의 스택과 관련된 행위를 지속할 수 있다.

 

top을 감소시키기 전에 pop되는 값을 나중에 리턴하기 위해 현재 top에 있는 데이터를 변수에 담아놓는다.

top을 감소시킨 후에 push와 마찬가지로 감소시킨 값만큼의 임시 배열을 선언해 기존의 스택 데이터를 마지막 값을 제외하고 담는다.

 

pop을 할 때 주의해야 할 점은 top의 값이 0인 경우이다.

top의 값이 0인 경우에는 현재 스택이 비었다는 것을 의미한다.

빈 스택에서 pop을 하여 데이터를 꺼낼 수 없으므로 예외처리가 필요하다.

 

과제 4. 앞서 만든 ListNode를 사용해서 Stack을 구현하세요.


  • ListNode head를 가지고 있는 ListNodeStack 클래스를 구현하세요.
  • void push(int data)를 구현하세요.
  • int pop()을 구현하세요.
package com.study.week4;

public class ListNodeStack {
    public ListNode head;

    public ListNodeStack() {
        this.head = new ListNode();
    }

    public void push(int data){
        ListNode node = new ListNode(data);
        node.next = head.next;
        head.next = node;
    }

    public int pop(){
        if (head.next == null){
            System.out.println("Empty Stack");
            return 0;
        }
        ListNode removedNode = head.next;
        int removed = removedNode.data;
        head.next = removedNode.next;
        removedNode.next = null;
        return removed;
    }

    public static void main(String[] args) {
        ListNodeStack stack = new ListNodeStack();
        stack.push(5);
        stack.push(52);
        stack.push(551);
        stack.push(333);
        stack.push(12);
        stack.push(66);

        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());

        stack.push(1222);
        stack.push(6146);

        System.out.println(stack.pop());
        System.out.println(stack.pop());
    }
}

앞서 구현했었던 스택을 배열이 아닌 연결리스트로 구현했다.

앞에서 만들었던 ListNode 클래스를 이용해서 쉽게 구현할 수 있었다.

 

개념은 위의 배열 스택과 같다.

연결리스트를 이용한 구현에서는 top 변수가 필요하지 않다.

왜냐하면 연결리스트에는 head 노드가 존재하기 때문이다.

push를 하든 pop을 하든 항상 head를 이용하면 가장 마지막 위치 (top 위치)를 유지할 수 있다.

과제 5. Queue를 구현하세요.


  • 배열을 사용해서 한번
  • ListNode를 사용해서 한번

[배열 큐]

package com.study.week4;

public class Queue {
    public int front;
    public int rear;
    public int q[];

    public Queue() {
        front = 0;
        rear = 0;
        q = new int[100];
    }

    public void enqueue(int data){
        if (rear >= q.length){
            q = new int[q.length * 2];
        }
        rear++;
        q[rear - 1] = data;
    }

    public int dequeue(){
        if (rear - front < 1){
            System.out.println("Empty Queue");
            return 0;
        }
        int removed = q[front];
        front++;

        return removed;
    }

    public static void main(String[] args) {
        Queue queue = new Queue();

        queue.enqueue(3);
        queue.enqueue(4);
        queue.enqueue(5);
        queue.enqueue(6);

        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
    }
}

[연결리스트 큐]

package com.study.week4;

public class ListNodeQueue {
    public ListNode head;
    public ListNodeQueue() {
        head = new ListNode();
    }

    public void enqueue(int data){
        ListNode node = new ListNode(data);
        for (ListNode n = head; n != null; n = n.next){
            if (n.next == null){
                node.next = n.next;
                n.next = node;
                break;
            }
        }
    }

    public int dequeue(){
        if (head.next == null){
            System.out.println("Empty Queue");
            return 0;
        }
        ListNode removed;
        removed = head.next;
        head.next = head.next.next;
        removed.next = null;

        return removed.data;
    }

    public static void main(String[] args) {
        ListNodeQueue queue = new ListNodeQueue();

        queue.enqueue(3);
        queue.enqueue(2);
        queue.enqueue(34);
        queue.enqueue(3636);
        queue.enqueue(6624);
        queue.enqueue(47111);

        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
    }
}

앞에서 스택을 구현했다면 이번에는 큐(Queue)를 구현했다.

스택과 비슷하지만 큐는 들어오고 나오는 곳이 분리되어 있고 나가는 순서도 다르다.

 

enqueue() : 삽입

큐에 데이터를 삽입하는 메소드이다.

배열에서는 front와 rear라고 하는 변수를 갖는다.

각각 큐의 맨 앞, 맨 뒤 인덱스를 갖는 변수들이다.

 

스택과 다르게 배열의 크기를 미리 정해주고 front와 rear 인덱스 값으로만 큐의 범위를 구분했다.

만약에 미리 정해둔 크기를 큐가 넘어간다면 현재 크기의 2배로 새로운 배열을 생성하도록 했다.

이렇게 구현했을 때 장점은 단순하게 front와 rear로 접근하여 enqueue, dequeue를 수행할 수 있기 때문에 빠르다.

만약 스택에서 했던 방식처럼 삽입, 삭제를 할 때마다 알맞은 크기의 배열을 할당하면,

객체 생성의 과부하와 매번 기존의 데이터를 새로운 배열에 옮기는 작업을 수행해야 했다.

 

그러나 미리 배열의 크기를 정해두고 인덱스로만 접근하면 직접적으로 접근이 가능해 매우 빠르고 코드가 간단하다.

 

단점은 사용하지 않는 배열의 공간들에 대한 낭비이다.

enqueue와 dequeue가 번갈아가면서 빈번해서 많은 공간을 사용하지 않는다면,

미리 선언한 배열의 많은 공간이 낭비될 것이다.

 

dequeue() : 삭제

큐에서 데이터를 삭제하는 메소드이다.

큐에서 데이터 삭제는 먼저 들어온 데이터가 가장 먼저 나간다.

이를 위해서 항상 마지막에 들어온 데이터를 head에 연결했다.

 

pop을 할 때는 head.next와 연결되어 있는 노드를 삭제하면 된다.

애초에 enqueue를 사용해 삽입할 때 head의 뒤에 삽입하기 때문에,

head.next가 가리키는 노드의 값이 최신일 것이다.                                                                                                                                                                         

반응형

관련글 더보기