HashSet

자바 Collection 중 Set의 대표적인 HashSet 클래스를 다루어 보도록 하겠습니다. HashSet은 Set의 파생클래스로 Set은 기본적으로 집합으로 중복된 원소를 허용하지 않습니다. HashSet은 순서 역시 고려가 되지 않습니다. 그렇다면 다음의 예제로 HashSet의 기본적인 동작을 살펴보도록 하겠습니다.

 

public static void main(String[] args){
	Set hashSet=new HashSet();
	hashSet.add("F");
	hashSet.add("B");
	hashSet.add("D");
	hashSet.add("A");
	hashSet.add("C");
	
	/* 위와 같은 데이터들을 다시 add */
	hashSet.add("F");
	hashSet.add("B");
	hashSet.add("D");
	hashSet.add("A");
	hashSet.add("C");
	
	/* HasSet의 "C"라는 원소 삭제 */
	hashSet.remove("C");
	
	/* HashSet 모든 원소 출력 */
	System.out.println("HashSet 원소 출력");
	Iterator it=hashSet.iterator();
	while(it.hasNext()){
		System.out.print(it.next()+" ");
	}
	
	/* HashSet의 모든 원소를 ArrayList로 전달 */
	List arrayList=new ArrayList();
	arrayList.addAll(hashSet);
	
	/* ArrayList의 모든 원소 출력 */
	System.out.println();
	System.out.println("ArrayList 원소 출력");
	for(int i=0;i<arrayList.size();i++){
		System.out.print(arrayList.get(i)+" ");
	}
}
 

 

우선 HashSet에 "F", "B", "D", "A", "C"라는 문자열을 차례대로 넣었습니다. 그리고 나서 다시 같은 데이터들을 넣게 되죠.  Set에는 중복을 허용하지 않는다고 하였으니 Set에는 현재 { "F", "B", "D", "A", "C" }가 존재합니다.

 

그 후에 "C"라는 문자열을 지우는 군요. 뭐 쉽습니다. "C"라는 원소를 삭제했으니 { "F", "B", "D", "A"}만이 남는것을 알 수 있겠군요. HashSet을 출력하는 부분을 보고 확인해보세요.

아, HashSet에는 순서가 없으므로 Iterator를 사용해서 집합안의 원소를 출력하고 있습니다. Iterator의 메소드는 3개가 다인데요.

hasNext() : 다음 원소가 남아있는지 여부를 알아냅니다. 다음 원소가 남았다면 true를 반환합니다.

next() : 다음 원소를 가져옵니다. 

remove() : 현재 반복자가 가리키고 있는 원소를 삭제합니다.

 

장난삼아서 ArrayList에 HashSet의 원소를 모두 전달해서 확인해봤습니다. 아래는 그 결과입니다.

HashSet 원소 출력
A B D F 
ArrayList 원소 출력
A B D F 

 

이제 우리는 단순히 String 타입의 자료형이 아닌 우리가 정의한 클래스의 객체를 HashSet의 원소에 넣고 싶습니다. 그렇다면 어떻게 중복된 원소인지 아닌지를 확인할 수 있을까요?

위에서 String 객체를 HashSet은 어떻게 중복된 원소라고 감지했을까요? HashSet이 알아서 판단해 줄까요?

 

우리가 다음과 같은 Person 클래스가 있다고 합시다.

 

class Person{
	String name; //이름
	int residentNumber; //주민번호
}

 

사람이 같은지 여부를 판별하는 것은 주민등록번호를 보고 알 수 있지요? 허나 Set에 단순히 이 Person 클래스를 넘겨준다면 중복 여부를 판별하지 못할 겁니다. Set이 Person 객체의 어떤 메소드를 호출해서 같은지 말지를 판변할 수 있으니까요. 그 메소드가 바로 equals와 hashCode입니다.

 

1. equals(Object o)

만약 두 객체(현재 이 객체와 인자로 넘어온 객체 o)가 같다는 것을 알려주려면 equals 메소드에서 true를 반환해주어야합니다.

 

2. hashCode()

만약 equals에서 두 객체가 같다라고 true를 반환했다면 hashCode는 두 객체에서 항상 같은 값을 반환해야합니다. 만일 equals에서 false를 반환하여 같지 않다고 반환했다면 hashCode 역시 다르게 반환하는 것이 좋습니다.

 

위의 조건을 고려해서 구현한 Person 클래스를 다시 봅시다.

class Person{
	String name; //이름
	int residentNumber; //주민번호
	
	public Person(String name,int residentNumber){
		this.name=name;
		this.residentNumber=residentNumber;
	}
	@Override
	public int hashCode(){
		/* Objects.hash 메소드로 residentNumber의 해쉬값 반환 */
		return Objects.hash(residentNumber);
	}
	
	@Override
	public boolean equals(Object o){
		/* 주민 번호가 같은 Person은 true 반환 */
		Person p=(Person)o;
		return p.residentNumber==this.residentNumber;
	}
}

 

equals와 hashCode를 Override한 코드를 확인해봅시다.

equals에서는 단순히 residentNumber만 비교해서 같다면 true, 다르다면 false입니다.

hashCode에서는 Objects.hash 메소드로 residentNumber의 해쉬값을 반환합니다. 이 hash는 residentNumber가 다르다면 항상 다른 값이 반환됩니다.

 

 

이제 Set에 원소를 넣고 확인해보도록 하지요.

 

public static void main(String[] args){
	
	Set hashSet=new HashSet();
	hashSet.add(new Person("reakwon",111111));
	hashSet.add(new Person("KDC",222222));
	hashSet.add(new Person("KSG",333333));
	hashSet.add(new Person("reakwon",111112));
	hashSet.add(new Person("MJW",111111));
	
	Iterator it=hashSet.iterator();
	while(it.hasNext()){
		Person p=it.next();
		System.out.println(p.name+"/"+p.residentNumber);
	}
}

 

동명이인이 있군요. 주민 번호 111111인 reakwon과 주민 번호 111112인 reakwon인 객체들이네요. 우리는 이름이 같은것은 같은 객체로 보지 않습니다. 주민 번호가 같은 객체만 중복된 원소로 보는 것이죠.

그래서 이 두 객체는 집합에 추가가 됩니다.

 

하지만 이름이 주민번호가 111111의 "MJW"라는 객체와 주민번호 111111의 "reakwon"라는 객체는 같습니다. 주민번호가 같기때문입니다. 그래서 "MJW"라는 Person객체는 집합이 추가하지 않습니다.

 

그 집합의 모든 원소를 출력한 결과는 아래와 같습니다.

KSG/333333 
reakwon/111112 
reakwon/111111 
KDC/222222

 

이제 우리가 만든 객체들도 HashSet에 추가할 수 있습니다.

 

HashSet을 알아보았으니 다음은 HashMap에 대해서 알아볼 차례겠군요. 아래의 링크로 들어가서 개념과 사용법을 익혀보도록 합시다.

reakwon.tistory.com/151

 

[자바] 해시맵(HashMap)의 개념과 사용 예, 기본 메소드와 같은 키인지 판별하는 방법

해시맵(HashMap) 해시맵은 이름 그대로 해싱(Hashing)된 맵(Map)입니다. 여기서 맵(Map)부터 짚고 넘어가야겠죠? 맵이라는 것은 키(Key)와 값(Value) 두 쌍으로 데이터를 보관하는 자료구조입니다. 여기서

reakwon.tistory.com

반응형
블로그 이미지

REAKWON

와나진짜

,

컬렉션(Collection)

이름에서도 알 수 있듯이 자료들을 효율적으로 모을 수 있는 자료구조를 의미합니다. Collection의 상속구조는 아래의 그림과 같습니다.

 

주황색 상자는 인터페이스, 파란색 상자는 클래스를 의미하며 파란색 화살표는 extends, 녹색 화살표는 implements의 관계를 나타냅니다.

 

Collection이라는 인터페이스는 Collection 계층의 최상위 인터페이스입니다. Iterable이라는 인터페이스를 상속했다는 것을 알 수 있고, Map은 Collection 인터페이스를 상속하지는 않지만 자바의 JCF(Java Collections Framework)에 포함됩니다. 

 

 

Collection 인터페이스의 메소드들을 간단히 살펴보도록 하겠습니다. 별로 어려운 메소드는 없을 것 같습니다.

boolean add(E e)

요소를 추가합니다.

boolean addAll(Collection<? extends E> c)

Collection 타입의 매개변수에 있는 모든 요소를 추가합니다.

void clear()

Collection의 모든 요소를 지웁니다.

boolean contains(Object o)

인자 o가 이 Collection에 속한다면 true를 반환하고 없다면 false를 반환합니다.

boolean containsAll(Collection<?> c)

Collection c의 원소들이 이 Collection에 모두 존재한다면 true, 그렇지 않으면 false를 반환합니다.

boolean equals(Object o)

o와 같은 객체인지 아닌지 확인합니다.

int hashCode()

이 Collection의 해쉬코드를 반환합니다.

boolean isEmpty()

이 Collection이 비어있는지 확인합니다.

Iterator<E> iterator()

이 Collection의 반복자(Iterator)를 반환합니다. Set같은 Collection은 순서를 고려하지 않기때문에 iterator로 원소를 순회합니다. Iterator의 대표적인 메소드는 hasNext와 next가 있습니다.

boolean remove(Object o)

o와 같은 원소가 이 Collection에 존재한다면 삭제합니다.

boolean removeAll(Collection<?> c)

이 Collection에 Collection c를 모두 제거합니다.

boolean retainAll(Collection<?> c)

매개변수 c의 요소들만을 남겨둡니다.

int size()

원소의 사이즈를 반환합니다.

Object[] toArray()

이 Collection의 모든 요소를 포함하는 배열로 반환합니다.

<T> T[] toArray(T[] a)

지정한 타입의 배열로 변환합니다.

 

 

이제 Collection을 상속받는 List, Set, Queue와 Map에 관한 간단한 설명을 시작하도록 하겠습니다. 한 번더 이야기하자면 아래의 Collection은 전부 인터페이스로 스스로 객체생성이 불가능하다는 것을 알아두세요.

1. List

순서가 중요한 Collection입니다. 순서가 있는 데이터의 집합으로 데이터의 중복을 허용합니다. 기존의 배열과는 다르게 크기가 동적으로 변합니다.

 

파생클래스

Vector, ArrayList : 두 클래스는 같이 설명하겠습니다. 우선 기본적인 동작은 원소를 추가, 삭제하는 것은 둘 다 비슷합니다. 하지만 Vector는 동기화 처리가되어있습니다. 즉, 하나의 스레드만이 Vector에 요소를 추가하거나 삭제가 가능하지요. 스레드에 대해서 안전하지만 느리다는 단점이 있습니다. 하지만 ArrayList는 동기화가 되어있지 않습니다. 그렇기 스레드에 대해 안전하지 않습니다. 하지만 추가, 삭제가 빠르다는 장점이 있습니다.

단일쓰레드 환경에서 개발할때에는 ArrayList를 쓰는것이 바람직하다고 할 수 있습니다.

LinkedList : 자료구조를 배웠다면 이해가 수월할 것인데, 간단히 말하면 각각의 요소는 다음 요소를 가리키고 있어 추가, 삭제 연산이 빠릅니다.

 

 

2. Set

순서가 없는 데이터의 집합으로 데이터의 중복을 허용하지 않습니다. 우리가 중딩시절에 배운 집합을 떠올리면 될텐데요. 참고로 저는 집합이 나오고 수학을 포기했습니다.

 

파생클래스

HashSet : 내부적으로 해싱을 이용해서 구현된 클래스입니다. Set 파생클래스에서 가장 성능이 우수합니다.

TreeSet : 내부적으로 레드-블랙 트리 방식으로 구현된 클래스입니다. 레드- 블랙 트리는 이진 탐색 트리의 일종으로 log(n)의 속도로 삽입, 삭제, 검색이 가능합니다. HashSet보다는 성능이 느립니다.

 

3. Queue

아마 너무나도 잘 알고 있을 Queue는 기본적으로 선입 선출(First-In First-Out) 형식의 자료구조입니다. 먼저 들어온 원소가 먼저 나간다는 Collection입니다.

 

파생클래스

PriorityQueue : 들어온 순서가 아니라 우선순위 별로 Queue에서 원소를 꺼내옵니다. 우선순위는 Comparable 인터페이스로 정할 수 있습니다.

ArrayDeque : 보통의 큐와는 다르게 큐의 양쪽에서 원소를 꺼내올 수 있는 Collection입니다.

 

4. Map

Map은 키-데이터(Key-Value) 쌍으로 자료를 보관하고 있습니다. Map에서 순서는 고려되지 않는 편이며 키는 중복을 허용할 수 없습니다.

 

파생클래스

HashMap : 키-값의 쌍으로 값을 가져올 수 있는 대표적인 Map의 Collection입니다. 동기화를 보장하지 않습니다. 키-값으로 쉽게 데이터를 검색할 수 있습니다. 단일 스레드에서 개발한다면 HashMap을 사용합시다.

또한 HashMap은 특이하게도 키 또는 값에 null을 저장할 수 있다는 점입니다.

Hashtable : HashMap과 사용법이 거의 동일한데, 다른점은 동기화를 보장한다는 것입니다. 그렇기 때문에 HashMap보다는 무겁겠죠?

또 HashMap과는 다르게 키 또는 값에 null을 사용할 수 없습니다.

SortedMap : 이름에서도 알 수 있듯이 정렬이 된 Map구조입니다. 

 

다음 포스팅에는 파생클래스를 사용하는 방법에 대해 알아보도록 하겠습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

소켓 통신에 대한 개념과 예제가 더 많은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

소켓(socket)

네트워크 통신을 하는 표준 방법으로 프로세스간 연결의 종점이라고 볼 수 있습니다. 기본적인 개념은 아래의 그림과 같습니다. 

위의 그림은 TCP/IP에서의 인터넷 통신을 보여줍니다. 클라이언트의 컴퓨터의 물리적 주소(MAC 주소) DD-44-EE-55-FF-66이며 논리적 주소(IP 주소)는 10.2.2.2입니다. 클라이언트는 여러가지의 프로그램을 실행시키고 있는데 그 중 어떤 프로세스는 TCP 포트번호 12345를 사용합니다. 이 클라이언트 프로세스는 물리적 주소가 11-AA-22-BB-33-CC이며 논리적 주소 10.1.1.3인 서버 컴퓨터의 포트 번호 80번을 사용하는 서버 프로세스와 연결되어 있습니다.

 

구체적으로 어떻게 통신할까요? 각 프로세스는 소켓을 통해서 통신을 하게 되는데, 소켓은 간단히 얘기해 ip주소와 포트번호를 갖고 있는 인터페이스라고 생각하면 됩니다. 소켓은 리눅스에서 파일로 다루어지며 프로세스는 이 소켓을 사용할때 파일디스크립터를 통해 사용합니다. 우리는 리눅스 파일 입출력에 대해 배울때 파일디스크립터를 사용했지요? 소켓 역시 파일디스크립터를 이용해서 읽기, 쓰기가 가능합니다.

 

소켓 통신할때 필요한 주요함수는 무엇이 있을까요? 간단히 알아보도록 합시다. 

 

1. socket(int domain, int type, int protocol)

소켓을 만드는데 바로 이 함수를 사용합니다. 소켓 역시 파일로 다루어지기 때문에 반환값은 파일디스크립터입니다. 만약 소켓을 여는데 실패했다면 -1을 리턴합니다.

 

2. connect(int fd, struct sockaddr *remote_host, socklen_t addr_length)

원격 호스트(원격 컴퓨터)와 연결하는 함수입니다. 연결된 정보는 remote_host에 저장됩니다. 성공시 0, 오류시 -1을 반환합니다.

 

3. bind(int fd, struct sockaddr *local_addr, socklen_t addr_length)

소켓을 바인딩합니다. 이렇게 생각하면 됩니다. 지금 fd로 넘겨지는 소켓과 이 프로세스와 묶는다(bind)라고 생각하시면 됩니다. 그래서 해당 프로세스는 소켓을 통해 다른 컴퓨터로부터 연결을 받아들일 수 있습니다.

 

4. listen(int fd, int backlog_queue_size)

소켓을 통해 들어오는 연결을 듣습니다. backlog_queue_size만큼 연결 요청을 큐에 넣습니다. 성공시 0, 오류시 -1을 반환합니다.

 

5. accept(int fd, sockaddr *remote_host, socklen_t *addr_length)

어떤 컴퓨터에서 이 컴퓨터로 연결할때 연결을 받아들입니다. 함수 이름이 말해주고 있죠.

연결된 원격 컴퓨터의 정보는 remote_host에 저장됩니다. 오류시에 -1을 반환합니다.

 

6. send(int fd, void* buffer, size_t n, int flags)

buffer를 소켓 파일 디스크립터인 fd로 전송합니다. 보낸 바이트수를 반환하며 실패시 -1을 반환합니다.

 

7. recv(int fd, void* buffer, size_t n, int flags)

send함수와 사용법이 거의 비슷합니다. n바이트를 buffer로 읽습니다. 성공시 받은 바이트수를 반환하며 실패시 -1을 반환합니다.

 

 

 

이제 예제를 보며 더 자세한 설명을 하도록 하죠. 다음 예제는 서버 프로그램이 클라이언트 프로그램에서 전송한 메시지를 출력해주는 소스코드입니다. 클라이언트 프로그램은 텔넷을 사용할 것이기 때문에 따로 클라이언트 프로그램 소스코드는 없습니다.

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 12346
#define BUF_SIZE 1024
int main(void){
        int socket_fd,accepted_fd;
        struct sockaddr_in host_addr, client_addr;
        socklen_t size;
        int recv_length;
        char buffer[BUF_SIZE];

        socket_fd=socket(PF_INET,SOCK_STREAM,0);

        host_addr.sin_family=AF_INET;
        host_addr.sin_port=htons(PORT);
        host_addr.sin_addr.s_addr=0;
        memset(&(host_addr.sin_zero),0,8);

        bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr));

        listen(socket_fd,3);

        while(1){
                size=sizeof(struct sockaddr_in);
                accepted_fd=accept(socket_fd,(struct sockaddr *)&client_addr,&size);

                send(accepted_fd,"Connected",10,0);
                printf("Client Info : IP %s, Port %d\n", inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));

                recv_length=recv(accepted_fd,&buffer,BUF_SIZE,0);
                while(recv_length>0){
                        printf("From Client : %s\n",buffer);
                        recv_length=recv(accepted_fd,&buffer,BUF_SIZE,0);
                }

                close(accepted_fd);
        }
        return 0;
}

 

여기서 포트번호는 12346을 사용한다고 하겠습니다.

 

socket_fd=socket(PF_INET,SOCK_STREAM,0);

socket의 첫번째 인자는 프로토콜 체계 PF(Protocol Family)를 지정합니다. PF_INET은 인터넷 IP프로토콜 체계입니다. 사용하는 프로토콜 체계에는 여러가지가 있습니다. IP외에도 1970년대에 개발된 공중 데이터 네트워크에 대한 표준 X.25 외에도 애플토크,XEROX 네트워크 등등 있는데 우리는 IP를 사용할 것이기 때문에 PF_INET만 사용할 것입니다.

 

두번째 인자는 소켓의 타입입니다. 가장 보편적으로 사용하는 타입은 Stream과 Datagram입니다. SOCK_STREAM은 연결형, SOCK_DGRAM은 비연결형이라고 생각하면 되겠습니다.

 

세번째 인자는 프로토콜로, 일반적으로 0을 넣어주면 시스템이 자동으로 설정해줍니다.

host_addr.sin_family=AF_INET;
host_addr.sin_port=htons(PORT);
host_addr.sin_addr.s_addr=0;
memset(&(host_addr.sin_zero),0,8);

bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr));

 

다음은 바인드할때 구조체를 넘겨야하는데요. 이 프로세스가 사용할 소켓 fd와 컴퓨터의 IP주소, 포트와 묶는 작업이라고 보면 됩니다.

 

우리는 TCP/IP 상에서의 통신이기 때문에 IPv4용 구조체인 sockaddr_in을 사용합니다.

sin_family에는 IP용 Address Family(AF_INET)을 지정합니다. 

sin_port는 이 프로세스가 사용할 포트번호를 지정합니다.

sin_addr.s_addr에는 주소가 들어가게 되는데요. 0은 현재 컴퓨터의 주소를 자동으로 채우라는 의미입니다. 그것이 아니라면 주소를 직접 지정해주어야합니다.

 

htons?

sin_port에서 htons는 무슨 함수일까요? 이 함수의 풀 네임은 host-to-network short로 16비트 정수를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환하는 함수입니다.

AF_INET 소켓 주소 구조체에서 사용되는 포트 번호와 IP주소는 빅 엔디언(Big-Endian)이라는 네트워크 바이트 순서를 따릅니다. 이것은 보통 우리가 사용하는 x86의 리틀 엔디언과는 반대의 표기법이죠. 그래서 변환없이 그대로 사용하게 되면 바이트 순서가 달라지게 되어 제대로 동작하지 않습니다.

 

이제 bind를 호출하는데 두번째 인자를 보세요. (struct sockaddr*)로 형 변환하고 있습니다. host_addr이라는 구조체 변수는 sockaddr_in이라는 구조체입니다.

bind는 TCP/IP뿐만 아니라 X.25, 애플토크 등 여러 프로토콜이 존재하기 때문에 인자로 받아야할 구조체 형이 sockaddr_in뿐만이 아닙니다. 그래서 일반화된 구조체가 필요하게 되는데 그 구조체가 sockaddr입니다.

 

sockaddr_in은 sockaddr로 형변환할 수 있습니다. 왜냐하면 구조체의 크기가 같기 때문이죠. sockaddr 구조체는 2바이트의 Address Family와 14바이트의 주소를 사용합니다. 반면 sockaddr_in의 IPv4 전용 구조체는 sa_data의 주소를 포트번호, ip주소, 기타 추가 비트를 포함하고 있죠.  우리는 일반화된 sockaddr에 sa_data에 직접 포트번호와 주소를 읽고 쓰기가 상당히 불편합니다.

그래서 더 사용하기 편한 인터넷 전용 구조체를 사용합니다. 형변환에 문제가 없게 크기를 같게 만들어 호환성에 문제가 없습니다.

 

listen(socket_fd,3);

 

그 소켓으로 들어오는 연결을 기다립니다. 마지막인자는 백로그 큐의 최대크기입니다.

while(1){
	size=sizeof(struct sockaddr_in);
	accepted_fd=accept(socket_fd,(struct sockaddr *)&client_addr,&size);

	send(accepted_fd,"Connected",10,0);
	printf("Client Info : IP %s, Port %d\n", inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));

	recv_length=recv(accepted_fd,&buffer,BUF_SIZE,0);
	while(recv_length>0){
		printf("From Client : %s\n",buffer);
		recv_length=recv(accepted_fd,&buffer,BUF_SIZE,0);
	}

	close(accepted_fd);
}
        

 

 

 

accept를 통해서 이제 새롭게 연결된 클라이언트 전용 파일디스크립터를 얻어옵니다. 클라이언트에 대한 정보는 client_addr에 저장됩니다.

 

연결이 성공돼었다면 send를 통해서 "Connected"라는 문자열을 클라이언트 쪽으로 보냅니다.

그 후 클라이언트의 정보를 출력하지요. 두가지를 출력합니다. IP주소와 Port번호입니다.

 

inet_ntoa(struct in_addr *network_addr)

네트워크 주소를 숫자사이의 점을 찍는 IP주소로 network to acsii라는 뜻입니다. ip주소가 담긴 in_addr구조체는 32비트의 네트워크 주소를 갖고 있기 때문에 숫자사이에 점을 찍는 형태로 바꾸려면 이 함수를 사용합니다.

 

ntohs(Network-to-Host Short)

이것 역시 네트워크 바이트 순서가 빅 엔디안이고, 호스트의 바이트 순서가 리틀 엔디안일때 변환해야할때 사용합니다.

 

그 후에는 계속 recv를 통해 클라이언트에서 입력받은 메시지를 서버에서 그대로 출력해줍니다.

 

결과

서버

# gcc server.c
# ./a.out

 

클라이언트

# telnet 192.168.10.131 12348
Trying 192.168.10.131...
Connected to 192.168.10.131.
Escape character is '^]'.

Connected

 

서버

Client Info : IP 192.168.10.131, Port 43774

 

 

클라이언트

Hi
I'm Reakwon

 

서버

From Client :

From Client : Hi

From Client : I'm Reakwon

 

클라이언트에서 타이핑한 것이 서버에서 그대로 출력이 되는 것을 볼 수 있습니다.

 

이상으로 간단히 리눅스의 소켓과 그에 대한 예제를 보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

파이프와 더불어 더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

파이프(Pipe)

파이프(Pipe)란 프로세스간 통신을 할때 사용하는 커뮤니케이션의 한 방법입니다. 가장 오래된 UNIX 시스템의 IPC로 모든 유닉스 시스템이 제공합니다. 하지만 두가지 정도의 한계점이 있습니다.

 

첫번째 한계점으로 파이프는 기본적으로 반이중 방식입니다. 물론 전이중 방식을 지원하는 시스템이 있긴 하나, 최대의 이식성을 위해서는 파이프는 반이중 방식이라는 생각을 해야합니다. 이것은 FIFO라는 명명된 파이프로 극복할 수 있습니다.

두번째 한계점으로는 부모, 자식 관계에서의 프로세스들만 사용할 수 있습니다. 부모프로세스가 파이프를 생성하고, 이후 자식 프로세스와 부모프로세스가 파이프를 이용하여 통신합니다.

 

이러한 한계점이 잇긴 하지만 여전히 쓸모있는 IPC기법입니다.

 

파이프는 unistd.h 헤더파일이 존재합니다.

 

#include <unistd.h>
int pipe(int fd[2]);

pipe함수가 성공적으로 호출되었다면 0, 실패했을 경우 -1을 반환합니다.

인자 fd는 2개의 원소가 있는 배열이라는 점을 주목합시다. 2개의 원소를 쓰는 이유가 있습니다. 아래의 그림을 보면서 이해합시다.

 

 

파이프는 커널영역에 생성되어 파이프를 생성한 프로세스는 파일 디스크립터만 갖고 있게 됩니다. 여기서 파일디스크립터 fd[1]은 쓰기용 파이프, fd[0]은 읽기용 파이프입니다. 그러니 우리가 만약 데이터를 fd[1]에 쓰게 되면 fd[0]으로 그 데이터를 읽을 수 있는 것입니다.

 

그렇다면 자식 프로세스를 하나 더 두어서 자식과 부모가 통신할 수 있게 하려면 어떻게 해야할까요? 우선 자식 프로세스를 fork하면 파일 디스크립터는 부모의 파일디스크립터를 자식이 그대로 사용할 수 있는 것을 활용합니다. (파일디스크립터가 그대로 자식프로세스에 복제됩니다.)

부모프로세스는 파이프에 데이터를 쓰는 프로세스, 자식 프로세스는 그 파이프에서 데이터를 읽는 프로세스로 설계합시다.

 

 

우선 부모 프로세스에서 파이프를 생성하면 파이프에 데이터를 쓸것이기 때문에 읽기 파이프는 닫습니다. fd[0]이죠? 그런 후 fd[1]에 데이터를 씁니다.

자식 프로세스는 쓰기 파이프는 쓰지 않으므로 fd[1]을 닫고, 읽기 파이프로 데이터를 읽습니다.

 

다음은 그런 기능을 하는 코드입니다.

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_BUF 1024
#define READ 0
#define WRITE 1
int main(){
        int fd[2];
        pid_t pid;
        char buf[MAX_BUF];

        if(pipe(fd) < 0){
                printf("pipe error\n");
                exit(1);
        }
        if((pid=fork())<0){
                printf("fork error\n");
                exit(1);
        }

        printf("\n");
        if(pid>0){ //parent process
                close(fd[READ]);
                strcpy(buf,"message from parent\n");
                write(fd[WRITE],buf,strlen(buf));
        }else{  //child process
                close(fd[WRITE]);
                read(fd[READ],buf,MAX_BUF);
                printf("child got message : %s\n",buf);
        }
        exit(0);
}

 

결과는 아래와 같습니다.

 

child got message : message from parent

 

자식 프로세스에서 부모 프로세스가 pipe에 쓴 데이터를 읽었습니다. 

또는 자식프로세스가 데이터를 쓰고, 부모프로세스가 데이터를 읽는 설계도 가능하겠죠.

 

 

그렇다면 부모 프로세스와 자식 프로세스가 읽기, 쓰기가 가능하게 구현하려면 어떻게 해야할까요? 파이프를 한개만 사용한다고 해봅시다.

 

그리고 이런 상황을 가정해보지요.

1. 먼저 부모프로세스가 파이프에 fd[1]로 데이터를 보냅니다. 

2. 그 이후 자식 프로세스가 부모 프로세스가 쓴 데이터를 fd[0]으로 읽습니다.

3. 자식 프로세스는 바로 fd[1]로 파이프에 응답값을 보냅니다.

4. 부모 프로세스는 fd[0]으로 자식 프로세스가 보낸 응답값을 읽습니다.

 

결론을 말씀드리면 항상 위의 상황은 발생하지 않습니다. 그 이유는 누가 먼저 파이프를 읽느냐에 따라서 결과가 달라지는데, 만일 부모프로세스가 파이프에 쓰고, 자식 프로세스가 그 데이터를 읽기도 전에 부모프로세스가 먼저 데이터를 읽는다면 파이프에 데이터는 없겠죠. 허나 자식 프로세스는 없는 데이터를 계속 읽기만 기다리고 있기 때문에 프로그램이 망하게 되는 겁니다.

 

이때는 파이프를 2개 사용해야합니다.

fdA와 fdB 2개 사용합니다. 부모프로세스는 자식에게 쓰기용으로 fdA[1], 자식프로세스로부터 읽기용으로 fdB[0]만 있으면 됩니다. 필요없는 fdA[0], fdB[1]은 닫아줍니다.

그리고 자식프로세스는 부모프로세스로부터 읽기용으로 fdA[0], 쓰기용으로 fdB[1]만 있으면 되지요. 역시 필요없는 fdA[1], fdB[0]은 닫아줍니다.

이제 이런 개념으로 코드를 구현합시다.

 

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_BUF 1024
#define READ 0
#define WRITE 1
int main(){
        int fdA[2],fdB[2];
        pid_t pid;
        char buf[MAX_BUF];
        int count=0;

        if(pipe(fdA) < 0){
                printf("pipe error\n");
                exit(1);
        }

        if(pipe(fdB) < 0){
                printf("pipe error\n");
                exit(1);
        }

        if((pid=fork())<0){
                printf("fork error\n");
                exit(1);
        }

        printf("\n");
        if(pid>0){ //parent process
                close(fdA[READ]);
                close(fdB[WRITE]);
                while(1){
                        sprintf(buf,"parent %d",count++);
                        write(fdA[WRITE],buf,MAX_BUF);
                        memset(buf,0,sizeof(buf));
                        read(fdB[READ],buf,MAX_BUF);
                        printf("parent got message : %s\n",buf);
                        sleep(1);
                }
        }else{  //child process
                close(fdA[WRITE]);
                close(fdB[READ]);
                count=100000;
                while(1){
                        sprintf(buf,"child %d",count++);
                        write(fdB[WRITE],buf,MAX_BUF);
                        memset(buf,0,sizeof(buf));
                        read(fdA[READ],buf,MAX_BUF);
                        printf("\tchild got message : %s\n",buf);
                        sleep(1);
                }
        }
        exit(0);
}

 

 

부모 프로세스는 0부터 1초마다 증가한 값을 파이프에 쓰고, 자식 프로세스로부터 파이프로 읽습니다. 자식 프로세스는 100000부터 증가한 값을 1초마다 쓰고, 읽습니다. 그 결과는 아래와 같습니다.

        child got message : parent 0
parent got message : child 100000
        child got message : parent 1
parent got message : child 100001
        child got message : parent 2
parent got message : child 100002
        child got message : parent 3
parent got message : child 100003
        child got message : parent 4
parent got message : child 100004
        child got message : parent 5
parent got message : child 100005

       

부모 자식 관계의 프로세스가 아닌 별개의 프로세스가 통신할때는 아까 위에서 말씀드린 것 처럼 FIFO를 사용해야합니다. 

 

파이프를 이용한 IPC구현, 이제 어렵지 않겠죠?

 

반응형
블로그 이미지

REAKWON

와나진짜

,

SSH(Secure Shell)

SSH란 원격에서 서버로 터미널에 접속할때 암호화를 통해 안전한 접속환경을 제공하여 telnet과 같은 안전하지 않은 원격 터미널 접속 프로그램을 보완하는 프로토콜 또는 프로그램입니다. SSH는 기본적으로 포트 22번을 사용합니다.

 

원격에서 telnet으로 서버에 접속할 경우 암호화 과정을 거치지 않으므로 스니핑을 한다면 계정과 비밀번호가 고스란히 노출됩니다. 하지만 ssh를 사용하여 접속한다면 암호화가 되었기 때문에 스니핑을 하여 패킷을 보더라도 의미를 파악할 수 없습니다. 또한 강력한 인증기능까지 지원합니다.

 

SSH의 주요 기능은

  • 보안 접속을 통한 rsh, rcp, rlogin, rexec, telnet, ftp 등을 제공합니다.
  • IP spoofing을 방지하기 위한 기능을 제공합니다.
  • X11 패킷 포워딩 및 일반적인 TCP/IP 패킷 포워딩을 제공합니다.

[출저 : 위키백과]

 

그렇다면 telnet접속과 ssh접속은 어떻게 다른지 눈으로 직접 살펴봅시다.

 

 

telnet으로 서버접속( 클라이언트 IP : 110.13.7.47 , 서버 IP : 110.13.7.20)

window 클라이언트에서 cmd에서 telnet을 통해 서버로 접속합니다.

엔터를 치는 순간 네트워크에서는 어떤 페킷이 오고 갔을까요? 아래는 와이어샤크로 본 페킷들입니다. 이 페킷을 보면 localhost login: 이라는 문자열을 볼 수 있습니다. 

서버(110.13.7.20)에서 클라이언트(110.13.7.47)로 보내는 군요. 그렇다면 클라이언트의 telnet 터미널 화면은 어떻게 됐을까요?

localhost login: 이라는 로그인 화면으로 들어왔습니다. 그렇다면 위의 페킷은 로그인하라는 디스플레이 메시지를 보냈다는 것을 짐작할 수 있겠군요.

 

이제 로그인을 시도해보지요. 아이디는 reakwon, 비밀번호는 reakwon입니다. 여기서 클라이언트(110.13.7.47)에서 서버(110.13.7.20)로 보내는 페킷만을 봅시다.

자, r이라는 문자를 보내는 군요. 이후에 75.6.355213의 페킷을 봅시다. r문자를 보낸 이후의 페킷입니다.

e라는 문자가 찍혔네요. 이렇게 reakwon이라는 문자를 서버에 보내고 서버는 그에 대한 응답으로 다시 reakwon이라는 문자를 각각 echo합니다. 이렇게 유저의 계정은 찾을 수 있습니다.

 

비밀번호 역시 다르지 않습니다. 이후의 페킷을 보면 알 수 있습니다. 다음은 계정의 비밀번호를 알아낼 수 있는 페킷들입니다. 하나씩 보도록 하지요.

페스워드의 시작을 알리는 페킷이 나옵니다. 

이후 아까처럼 reakwon의 문자들을 연속해서 서버에 보냅니다. 비밀번호를 입력할땐 서버로 부터 echo가 오지 않는군요.

이제 망했습니다. 해킹당했군요.

 

 

ssh로 서버접속( 클라이언트 IP : 110.13.7.47 , 서버 IP : 110.13.7.20)

다음은 ssh로 접속한 페킷들입니다. 대충보아도 서버와 클라이언트의 페킷이 암호화되었다고 하는군요. SSH버전은 2입니다.

페킷 하나를 보면 이렇습니다. 음, 암호화된 페킷을 보면 34e96..., 메시지 인증 코드는 14b4... 입니다. 

암호화된 페킷이라 잘 모르겠습니다. 암호화를 하여 데이터를 전송하고 수신하는 것만 알면 되겠습니다.

CentOS7에서 SSH로 서버 접속

CentOS7에서 ssh연결은 다음과 같은 과정을 거치는데요.

1. openssh-server 설치

2. 방화벽 열기

3. 가상머신을 돌릴 경우

 

1. openssh-server 설치

일단 ssh 서버가 설치되어 있어야합니다. 우선 설치되어 있나 확인을 해봅시다. 

 

[root@localhost etc]# rpm -qa | grep openssh-server
openssh-server-7.4p1-16.el7.x86_64

 

이미 설치가 되어있군요. 설치가 안된 분들은 설치하면 됩니다. 

다음의 명령으로 ssh 서버를 설치하세요.

 

yum install openssh-server

 

 

 

 

2-1. 방화벽 포트 열기

CentOS7에서는 기본적으로 방화벽(firewalld)가 설치되어 있습니다. 혹여나 설치가 되어있지 않은 분들은 yum install firewalld 명령으로 방화벽을 설치합시다.

 

ssh서버와 firewalld가 설치가 되었다면 방화벽에 ssh가 사용하는 포트를 열어줘야합니다. 그래서 /etc/ssh/sshd_config 파일을 열어 

(루트 사용자라면 vi /etc/ssh/sshd_config, 아니면 sudo vi /etc/ssh/sshd_config) 

#semanage port -a -t ssh_port_t -p tcp #PORTNUMBER
#
#Port 22    <-- #해제
#AddressFamily any
#ListenAddress 0.0.0.0

 

에서 #Port 22의 주석을 해제합니다. 앞의 #을 지우게 되면 주석이 해제되는 겁니다. 이 후 sshd을 다시 시작해야되겠죠? 다음의 명령어로 sshd를 다시 시작합시다.

 

systemctl restart sshd.service

 

2-2. 방화벽 포트 열기

또는 다른 방법이 있는데요. 다음의 명령으로 방화벽에 명시해줍니다.

 

sudo firewall-cmd --permanent --add-service=ssh

 

이후 firewall의 변경사항을 적용하고 싶다면 

 

sudo firewall-cmd --reload

 

의 명령을 사용한 후 방화벽을 다시 시작해주어야합니다.

 

sudo systemctl restart firewalld

 

3. 혹시 가상머신을 사용해서 ssh를 쓰고 계신가요?

그럴 경우 설정-> 네트워크 -> 어댑터 -> 어댑터에 브릿지를 선택해주세요.

 

 

연결

자, 이제 putty라는 프로그램 또는 mobaXterm이라는 ssh 프로토콜이 가능한 프로그램을 사용해서 연결해봅시다. 일단 저의 ssh 서버의 주소는 ifconfig라는 명령어로 볼 수 있습니다. 

[root@localhost etc]# ifconfig
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 192.168.10.123  netmask 255.255.255.0  broadcast 192.168.10.255 

...

 

ssh의 서버주소는 192.168.10.123이군요. 이제 putty라는 프로그램으로 여기에 ssh로 접속해봅시다.

 

이제 open을 하게 되면 아래 처럼 로그인 화면이 나옵니다. 혹은 첫번째 실행이라면 뭐 yes, no를 묻는 대화창이 나오게 되는데 yes눌르시고 로그인하면 됩니다.

 

이제 원격에서도 ssh를 통해 서버에 접속 할 수 있습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

pwd (Print Working Directory)

현재 디렉토리를 알려주는 명령어입니다. 현재 유저가 어느 위치에 있는지 알아볼 수 있는 명령어입니다. 

 

아래의 예에서는 현재 작업디렉토리가 /home/reakwon이라는 디렉토리라는 것을 알 수 있습니다.

[reakwon@localhost ~]$ pwd

/home/reakwon

cd (Change Directory)

디렉토리를 이동하는 명령어입니다. 절대 경로나 상대 경로를 주어서 디렉토리를 이동할 수 있습니다. 

 

cd /root  = /root 디렉토리로 이동합니다.

cd ..  = 현재 디렉토리의 상위 디렉토리로 이동합니다.

cd ~ = 현재 사용자의 홈 디렉토리로 이동합니다.

 

이런 조합으로 이동이 가능합니다.

 

 

ls (List)

디렉토리 안의 파일과 디렉토리를 알 수 있는 명령어입니다. 여러가지 옵션이 존재하는데, 옵션은 여러가지를 혼합해서 사용할 수 있습니다. 옵션을 살펴보도록 하지요.

 

a : 모든 파일을 보여줍니다. 즉, 숨김파일까지 전부 보여주는 것이죠. 숨김파일이란 파일명 앞에 "."이 달린 파일명이고 ls명령어만으로 보여지지 않는 파일입니다.

 

l : (소문자 '엘') 파일의 자세한 내용을 보여줍니다. 

다음은 제 리눅스 pc의 tmp 디렉토리의 내용의 일부를 보여줍니다.

-rw-r--r--. 1 root    root    1148  5월 16 14:33 anaconda.log
drwx------. 2 reakwon reakwon   25  5월 16 14:50 firefox_reakwon
drwxr-xr-x. 2 root    root      18  5월 16 13:51 hsperfdata_root
-rw-r--r--. 1 root    root     420  5월 16 14:33 ifcfg.log
-rwx------. 1 root    root     836  5월 16 14:29 ks-script-o30BkB
-rw-r--r--. 1 root    root       0  5월 16 14:32 packaging.log
-rw-r--r--. 1 root    root       0  5월 16 14:32 program.log
-rw-r--r--. 1 root    root       0  5월 16 14:32 sensitive-info.log
drwx------. 2 reakwon reakwon   24  5월 16 14:34 ssh-TF6BwHbO3Rdo
drwx------. 2 reakwon reakwon   24  5월 16 14:41 ssh-UDg27UAVaZcs
drwx------. 2 reakwon reakwon   24  5월 16 23:13 ssh-bWUCqvi956kP
drwx------. 2 reakwon reakwon   24  5월 16 22:38 ssh-g8sqmExSvLtD
        1.    2.     3.         4.         5.          6.                  7.

 

1. 파일의 종류와 권한: 이 파일이 어떤 파일인지, 소유주, 소유주 그룹, 기타 다른 유저들이 이 파일을 사용할때의 권한을 보여줍니다. 

파일의 종류는 다음과 같습니다.

    a) - : 일반 파일

    b) d : 디렉토리

    c) b : block 장치 파일

    d) c : character 장치 파일

    e) l : 심볼릭 링크 파일

    f) p : 명명된 파이프

    g) s : 소켓 파일

 

권한은 다음과 같이 지정됩니다. 각 3개씩 권한이 나누어 지는데요.

rwx(파일 소유자의 권한)r-x(파일 소유 그룹의 권한)r-x(그외 사용자에 대한 권한)

r : read , w : write , x : execute

 

위의 파일 권한을 해석해본다면 파일 소유자는 읽기,쓰기, 실행 권한을 갖고 있네요. 파일 소유자가 있는 그룹은 이파일을 읽고, 실행만 할 수 있겠네요. 다른 사용자는 읽고, 실행만 할 수 있습니다.

 

2. 하드링크수를 보여줍니다. 

3. 파일의 소유주를 보여줍니다. 디렉토리를 만들거나 파일을 만든 계정입니다. 파일의 소유주는 chown명령어를 이용해 바꿀 수 있습니다.

4. 소유그룹을 보여줍니다. 파일 소유주의 그룹을 나타냅니다. 

5. 파일의 크기를 보여줍니다.

6. 최종 수정일, 시간을 보여줍니다.

7 마지막으로 파일명을 보여줍니다.

 

 

 

i : inode번호를 보여줍니다. 디렉토리에는 파일명과 해당 파일의 inode번호가 매핑되어 있는데, ls -l과 같이 파일의 자세한 정보를 볼 수 있는 것은 inode에 파일에 대한 메타데이터가 기록이 되기 때문입니다. 어떤 것들이 있는지는 아래와 같습니다.

  • 파일 모드 : 파일의 형식과 실행 권한
  • 링크 수 : 이 아이노드에 대한 디렉터리 참조 수
  • 소유자 계정 : 파일의 소유자
  • GID : 이 파일과 관계된 그룹 소유자
  • 파일 크기 : 파일의 바이트
  • 파일 주소 : 주소 정보
  • 마지막 접근(Access) : 마지막으로 파일에 접근한 시각
  • 마지막 수정(Modified) : 마지막으로 파일을 수정한 시각
  • 아이노드 수정(Changed) : 마지막으로 아이노드를 수정한 시각

: 하위 디렉토리의 내용까지 보여줍니다. 예를 들어 현재 디렉토리가 aaa이며 파일이 bbb,ccc 그리고 그 하위 디렉토리가 ccc이며 ccc의 디렉토리에 eee,fff 파일이 있다면 디렉토리 aaa의 파일을 전부 출력해주며 하위 디렉토리인 ccc의 내용까지 전부 출력해주는 옵션입니다.

: 디렉토리인지, 어떤 타입의 파일인지를 보여줍니다.

 

반응형
블로그 이미지

REAKWON

와나진짜

,

C언어와는 조금 다르게 C++의 함수는 조금 더 특별한 기능이 추가되었습니다. 뭐가 다를까요?

 

1. C언어에서는 함수명이 같으면 컴파일 에러가 나지만 C++에서는 함수명이 같아도 매개변수의 자료형, 갯수가 다르다면 같은 함수명을 쓸 수 있습니다. 이것이 함수 오버로딩(overloading)이라고 하지요.

2. C언어에서는 함수 인자에 default값을 줄 수 없습니다. 하지만 C++에서는 사용자가 매개변수에 값을 넘겨주지 않으면 자동으로 dafault값이 지정됩니다.

3. C언어에서와 다르게 참조자(reference)가 등장합니다. 변수의 별칭이라고나 할까요?

4. 함수의 호출시간을 줄이고자 inline함수가 등장합니다. inline함수는 일반 함수와는 다르게 함수의 호출부에 코드를 직접 삽입함으로써 함수가 호출되는 과정이 없습니다. 그러니까 실행속도가 일반함수 호출하는 것보다 빠르지요.

 

대략적인 설명은 여기까지 하겠습니다. 이제 하나하나 자세하게 살펴보도록 합시다.

 

 

1. 함수오버로딩(Overloading)

함수오버로딩은 개념이 꽤나 간단합니다. 예를 들어 두 수를 더해서 반환하는 sum이라는 함수가 있다고 합시다. 우리는 정수형, 실수형 모두 sum이라는 함수로 이름을 짓고 싶습니다. 왜냐면 자료형만 다를뿐 같은 기능을 하기 때문이죠. 이렇게 오버로딩의 개념이 등장합니다.

아래의 코드가 오버로딩을 보여줍니다.

 

#include <iostream>

using namespace std;

int sum(int a, int b) {
	return a + b;
}

double sum(double a, double b) {
	return a + b;
}

int sum(int a, int b, int c) {
	return a + b + c;
}

double sum(double a, double b, double c) {
	return a + b + c;
}

int main(void) {
	cout << sum(10, 20) << endl;
	cout << sum(10.1, 20.1) << endl;
	cout << sum(10, 20,30) << endl;
	cout << sum(10.1, 20.1,30.1) << endl;
}

 

함수의 인자들이 자료형이 다른 것도 있고, 인자의 갯수가 다른 것이 있습니다. 함수의 이름은 같은 것을 알 수 있네요.

여기서 주의해야할 점은 반환형이 달라도 오버로딩이 되지 않습니다. 항상 매개변수의 타입 또는 갯수가 달라야 오버로딩이 된다는 것을 알아두세요.

 

2. default 인자 값

인자를 전달하지 않으면 default로 지정된 값으로 인자 변수가 초기화됩니다. 이것도 역시 어렵지 않습니다. 

 

#include <iostream>
using namespace std;

int sum(int a, int b = 10) {
	return a + b;
}
int main(void) {
	cout << sum(20) << endl;
	cout << sum(20, 20) << endl;
}

 

함수의 인자 갯수는 2개이지만 sum(20)이라는 호출이 가능한 이유는 b가 10으로 default 값을 사용하기 떄문입니다. 또는 보통의 함수 호출과 같이 인자를 2개 주어서 사용할 수도 있습니다.

 

디폴트 인자값을 사용할때 주의할 점이라고 한다면 디폴트 인자값은 항상 오른쪽부터 왼쪽으로 써줘야한다는 것입니다. 

 

int sum(int a, int b = 10,int c) {
	return a + b + c;
}

int sum(int a, int b = 10, int c = 20) {
	return a + b + c;
}

 

첫번째 처럼 중간이 디폴트 인자값이 있다면 오류가 나게되고 항상 밑의 함수처럼 오른쪽부터 왼쪽으로 디폴트값을 써줘야한다는 겁니다.

 

 

3. 참조자(Reference)

참조자는 C++에서 새롭게 등장한 기능입니다. 변수의 별명이라고 생각하면 될 것 같습니다. 우리가 어떤 사람을 부를때도 별명을 부르곤 하잖아요? reference를 사용할땐 &를 사용하여 참조자라는 것을 알려줍니다.

예를 들어 이렇게 사용하죠.

 

int a=100;

int &b=a;

 

이렇게 b는 a와 같은 데이터를 가리키며 주소 역시 같습니다. 마치 포인터처럼요.

이것을 함수에 사용하게 되면 call-by-reference를 구현할 수 있게 됩니다. 우리는 call-by-reference를 C언어에서 포인터로 시험해봤지요.

이제 참조자를 통해서 구현해보도록 합시다.

 

#include <iostream>

using namespace std;

void swap(int &a, int &b) {
	int temp = a;
	a = b;
	b = temp;
}

int main(void) {
	int a = 100;
	int b = 200;

	cout << "a:" << a << ",b:" << b << endl;
	swap(a, b);
	cout << "a:" << a << ",b:" << b << endl;
}

 

실행시켜보면 아래와 같이 값이 바뀐 것을 알 수 있습니다. a와 b에 주소값을 swap함수에 넘기지 않고 말이죠.

 

결과

a:100,b:200
a:200,b:100
계속하려면 아무 키나 누르십시오 . . .

 

이 같이 참조자는 크기가 큰 구조체나 데이터를 매개변수로 전달할 때 유용합니다.

 

4. inline 함수

보통의 함수 호출 과정은 다음과 같습니다.

 

스택에 다시 돌아갈 주소를 저장. 매개변수 전달 -> 스택에 지역 변수 할당 -> 함수 실행 -> 반환값 전달 -> 다시 돌아갈 주소로 이동

 

함수의 실행부분이 꽤나 길다면 뭐 호출하는 시간은 신경쓰지 않아도 되지만 함수의 실행부가 짧지만 자주 호출된다면 호출되는 시간을 무시할 순 없겠죠.

 

inline함수는 함수의 호출시간을 줄여 보다 더 빠른 성능을 내기 위함입니다. inline함수는 함수 호출부에 함수 정의 부분을 직접 삽입하여 호출하는 방식으로 매크로 함수와 비슷합니다. 하지만 매크로 함수를 쓰면 괄호의 늪에 빠질 수도 있죠..

인라인 함수는 간단합니다. 기존의 함수를 선언하거나 정의하는 부분에 inline 키워드만 붙여주면 되니까요.

inline키워드는 선언 또는 정의하는 부분에 한번만 선언해주어도 됩니다.

 

 

#include <iostream>
using namespace std;

inline int sum(int, int);

int sum(int a, int b) {
	return a + b;
}
int main(void) {
	int sumValue;
    sumValue = sum(10, 20);
	cout << sumValue << endl;
}

 

실제 이 코드는 이렇게 동작하게 됩니다.

 

#include <iostream>
using namespace std;

int main(void) {

	int sumValue;
	{
		int a = 10; int b = 20;
		sumValue = a + b;
	}
	cout << sumValue << endl;
}

 

정말 단지 inline함수의 내용이 코드에 직접 삽입되는 형태지요.

 

지금까지 C++에서 함수의 특징에 대해서 알아보았습니다. 딱히 어려운건 없었죠?

반응형
블로그 이미지

REAKWON

와나진짜

,

String을 왜 인코딩하고 디코딩할까요? 인코딩과 디코딩을 해야하는 상황이 있습니다. 만일 DB가 한글을 지원하지 않는 경우 한글로 된 문자를 숫자로 encoding해서 DB에 저장하면 되고, 사용자에게 보여줄때는 다시 decoding해서 보여주면 됩니다.

또는 암호화할때 문자열을 encoding한 후에 암호화하게 됩니다.

이 밖에도 문자열을 encoding과 decoding을 해야할 상황이 있겠죠? 그러므로 문자열을 어떻게 encoding할지 decoding할지 알아보도록 하겠습니다.

 

byte[] getBytes()

byte[] getBytes(Charset charset)

byte[] getBytes(String charsetName)

문자열을 인코딩된 byte형태로 넘겨줍니다. 매개변수없이 그냥 getBytes()메소드를 사용하면 플랫폼에 따른 default charset을 사용합니다.

만일 특정 charset을 지정할 경우 인자를 받는 getBytes메소드를 사용하면 됩니다. ISO-8859-1, euc-kr, utf-8 등의 charset이 존재하는데 encoding과 decoding할때 이 chatset을 맞춰서 해야합니다. 그러지 않을 경우 문자가 깨지는 현상이 발생하게 됩니다. 

 

자, 그러면 이제 실제 프로그램을 짜면서 사용법을 알아보도록 합시다. 여기서는 가장 많이 사용하는 UTF-8로 charset을 지정했습니다.

public static void main(String[] args){
	String str="reakwon의 블로그";
	//default charset으로 인코딩된 바이트 배열	
	byte[] bytes=str.getBytes();
    //인코딩된 바이트 출력
	System.out.print("Default charset encoding: ");
	for(int i=0;i<bytes.length;i++)
		System.out.print(bytes[i]+" ");
	System.out.println();
	//default charset으로 디코딩된 문자열 출력
	String decoded=new String(bytes);
	System.out.println(decoded);
		
	System.out.println();
	try {
    	//UTF-8로 인코딩된 바이트 배열
		bytes=str.getBytes("UTF-8");
		System.out.print("UTF-8 charset encoding: ");
		for(int i=0;i<bytes.length;i++)
			System.out.print(bytes[i]+" ");
		System.out.println();
        //이 바이트 배열을 default charset으로 디코딩된 문자열 출력 : charset이 다르므로 한글이 깨짐.
		decoded=new String(bytes);
		System.out.println(decoded);
		//인코딩된 UTF-8로 디코딩되어 한글이 깨지지 않음.
		decoded=new String(bytes,"UTF-8");
		System.out.println(decoded);
			
	} catch (UnsupportedEncodingException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}

 

문자열은 한글이 섞여있는 문자열입니다. 우선 default charset으로 바이트 배열을 얻어오고 출력해줍니다. 인코딩된 바이트 배열을 다시 디코딩하여 출력해줍니다. String으로 decoding하는 방법은 무척 간단합니다. 아래의 String 클래스의 생성자를 사용하면 됩니다.

 

 

String(byte[] bytes) : default charset으로 decoding한 문자열

String(byte[] bytes, String charsetName) : 특정 charset으로 decoding한 문자열

 

try...catch 안에서는 UTF-8로 인코딩하고 인코딩된 값을 출력해줍니다. 그 이후 String 인코딩 된 값을 출력해주는데요. 만약 default charset으로 디코딩한 후 문자열을 출력하면 어떻게 될까요? 예상하셨겠지만 영어를 제외한 한글을 깨져서 나옵니다.

그래서 인코딩된 방식과 같이 UTF-8로 디코딩해야합니다. 그래야만 한글이 깨지지 않지요. 그래서 바로 위의 String의 2번째 생성자를 사용해서 charset을 지정합니다. 그 후 출력하면 정상적으로 한글이 출력됩니다.

결과를 확인해보세요.

 

결과

Default charset encoding: 114 101 97 107 119 111 110 -64 -57 32 -70 -19 -73 -50 -79 -41 
reakwon의 블로그

UTF-8 charset encoding: 114 101 97 107 119 111 110 -20 -99 -104 32 -21 -72 -108 -21 -95 -100 -22 -73 -72 
reakwon?쓽 釉붾줈洹?
reakwon의 블로그

 

결과를 들여다보면 영어는 ASCII, 그리고 한글은 2바이트를 사용하는 것을 알 수 있네요.

반응형
블로그 이미지

REAKWON

와나진짜

,

자바에서 문자열을 다루기 위해서는 String 클래스의 메소드를 사용합니다. 이번 포스팅에서는 String 메소드를 설명하고 간단한 사용법울 알아보도록 하겠습니다.

 

int length()

문자열의 길이를 반환합니다. C언어로 치면 strlen과 같은 메소드겠네요. 사용법이 너무 간단하므로 예를 보이지는 않겠습니다.

 

String substring(int beginIndex)

String substring(int beginIndex, int endIndex)

substring 메소드는 beginIndex부터 문자열끝까지의 문자열을 반환합니다. 만약 끝을 가리키는 endIndex를 사용한다면 문자열의 특정 구간의 문자열을 반환합니다. 아래는 사용 예를 보여줍니다.

 

String str="AABBCCDD";
System.out.println(str.substring(2));
System.out.println(str.substring(2, 4));

"AABBCCDD"의 substring(2)는 처음 나오는 B의 자리를 인자로 전달합니다. 문자열의 첫번째 인덱스는 0부터 시작한다는 것을 잊지마세요. 그러니 처음 나오는 A의 인덱스는 0입니다. 결과는 B부터 문자열의 끝까지 새로운 문자열을 반환합니다.

또 substring(2,4)는 2번째 인덱스부터 4번째 인덱스 전까지 새로운 문자열을 반환합니다. 주의해야할 것은 4번 인덱스 앞까지라는 것입니다. 그러므로 4번 인덱스의 문자는 포함되지 않습니다. 결과는 "BB"라는 문자열이 반환되겠네요.

 

위의 결과는 아래와 같습니다.

 

결과

BBCCDD
BB

 

 

 

 

char charAt(int index)

문자열에서 char 값이 필요할때 사용하는 메소드입니다. 인자 index는 문자열에서 한 문자를 뽑아낼 위치를 말합니다.

String str="AABBCCDD";
System.out.println(str.charAt(4));
System.out.println(str.charAt(str.length()-1));

 

예를 들어 "AABBCCDD"라는 문자열에서 4번째 인덱스의 char 값을 얻고 싶다면 인자로 4를 넣어주면 됩니다. 그렇다면 처음 나오는 'C'가 될겁니다. 또는 마지막 문자열의 문자를 추출해내고 싶다면 length() 메소드의 반환값에서 1을 뺀 값을 전달하면 제일 마지막 문자를 가져올 수 있습니다.

 

결과

C
D

 

String concat(String str)

문자열을 합치는 메소드입니다. str을 현재 문자열 뒤에 추가합니다. 사실 저는 별로 이 메소드를 사용하지는 않는데요. 자바에서는 문자열을 합칠때 + 연산으로 합칠 수 있기 때문이지요.

String str="AABBCCDD";
System.out.println(str.concat("EE"));
System.out.println(str+"EE");

 

아래에서 똑같은 결과를 볼 수 있지요.

 

결과

AABBCCDDEE

AABBCCDDEE

 

boolean contains(CharSequence s)

문자열에서 특정 문자열이 포함되어있는지 여부를 확인하려면 이 메소드를 사용하면 됩니다. 매개변수에 포함됐는지의 문자열을 전달하고 문자열이 포함되어 있다면 true, 포함되지 않았다면 false를 반환합니다.

 

String str="AABBCCDD";
System.out.println(str.contains("BC"));
System.out.println(str.contains("AD"));

"BC"는 "AABBCCDD"에 포함되어 있으므로 true를 반환할 것이고, "AD"라는 문자열을 포함되어 있지 않으니 false를 반환합니다.

 

결과

true
false

 

 

 

 

static String copyValueOf(char []data)

static String copyValueOf(char []data, int offset, int count)

이 메소드는 static 메소드입니다. char형 배열을 문자열로 변환할때 사용하는 메소드인데요. 그냥 char 배열을 전달하면 그 배열 자체를 문자열로 반환합니다.

특정 위치에서 몇개의 문자까지 문자열로 받고 싶다면 offset에 특정 문자열을 얻고 싶은 위치를, count에 몇개의 문자를 받을 지를 전달하면 되겠습니다.

char []charArray={'A','B','C','D','E','F'};
System.out.println(String.copyValueOf(charArray));
System.out.println(String.copyValueOf(charArray,2,4));		

 

결과를 보면 직관적으로 이 메소드가 어떤 기능을 하는지 알 수 있을 겁니다.

 

결과

ABCDEF
CDEF

 

String[] split(String regex)

String[] split(String regex, int limit)

정규표현식을 기준으로 문자열을 쪼갭니다. 정규표현식을 몰라도 됩니다. 만약 limit에 인자를 전달하면 그 limit까지만 문자열을 쪼갭니다.

 

String str="AABB__CCDD__EEFF";
String []arr=str.split("__");
for(int i=0;i<arr.length;i++)
	System.out.println(arr[i]);

"AABB__CCDD__EEFF"를 "__"로 분리시켜보면 세개의 문자열이 나옵니다. "AABB", "CCDD", "EEFF"가 그것들이죠. 이것은 한가지 예이므로 쉼표로 분리하고 싶다면 쉼표로 분리할 수도 있습니다.

 

결과

AABB
CCDD
EEFF

 

String toLowerCase()

String toLowerCase(Locale locale)

String toUpperCase()

String toUpperCase(Locale locale)

문자열을 전부 소문자 또는 전부 대문자로 변환된 문자열을 얻고 싶을 때 위의 메소드를 사용하면 됩니다.

소문자로 변환된 문자열을 얻고 싶을때는 toLowerCase, 대문자로 변환된 문자열을 얻고 싶을때는 toUpperCase를 사용하면 되는 것이죠. 메소드 인자는 Locale은 사실 써본적은 없습니다. 인자없는 toLowerCase, toUpperCase로도 충분해 보입니다.

String str1="AABBCCDD";
String str2="ffgghhii";
System.out.println(str1.toLowerCase());
System.out.println(str2.toUpperCase());

 

"AABBCCDD"는 toLowerCase로 소문자로 출력하고, "ffgghhii"는 toUpperCase로 모두 대문자로 출력합니다.

 

결과

aabbccdd
FFGGHHII

 

 

int compareTo(String anotherString)

문자열을 사전순으로 비교합니다. 만약 anotherString이 사전순으로 앞에 등장할때는 양수를 반환하고 사전순으로 늦게 등장할때는 음수를 반환합니다. 만약 anotherString이 현재 문자열과 정확히 같다면 0을 반환하게 됩니다. 문자열을 비교할때는 equals를 주로 사용하는데요. 사전순으로 비교하고 싶을땐 compareTo를 사용하면 됩니다.

 

String str1="BCD";
String str2="ABC";
String str3="BCD";
String str4="CDE";
System.out.println(str1.compareTo(str2));
System.out.println(str1.compareTo(str3));
System.out.println(str1.compareTo(str4));

 

"ABC"는 "BCD" 기준으로 사전상 앞에 등장하니까 양수, "CDE"는 "BCD" 기준으로 사전상 뒤에 등장하니까 음수가 반환됩니다. str1과 str3은 문자열이 같으므로 0이 반환됩니다.

 

결과

1
0
-1

 

 

 

 

int compareToIgnoreCase(String str)

만약 소문자, 대분자를 무시하고 비교하고 싶을때는 이 메소드를 사용하면 됩니다. 원래 "ABC"와 "abc"를 compareTo 메소드로 비교하면 0이 반환되지 않습니다만 이 메소드를 사용해서 비교한다면 0이 반환됩니다.

String str1="abc";
String str2="ABC";
String str3="abc";
String str4="aaa";
System.out.println(str1.compareToIgnoreCase(str2));
System.out.println(str1.compareToIgnoreCase(str3));
System.out.println(str1.compareToIgnoreCase(str4));

 

str4와 비교하는 것을 제외하고는 0을 반환하겠군요.

 

결과

0
0
1

 

 

boolean equals(Object anObjec)

boolean equalsIgnoreCase(String anotherString)

사전순으로 비교할 필요없이 단순히 문자열이 같은지 다른지 비교할때는 이 메소드를 씁니다. 역시 대소문자를 구분하지 않는다면 equalsIgnoreCase를 사용하면 됩니다. 같다면 true, 다르면 false를 반환합니다.

 

 

static String format(String format, Object ...args)

특정 format으로된 문자열을 얻고 싶을때 사용하는 메소드입니다. C언어에서 이것과 비슷한 기능을 하는 sprintf가 되겠네요. 

String str=String.format("%d+%d=%d", 1,2,1+2);
System.out.println(str);

 

C언어를 사용했던 분들에게는 조금 익숙한 메소드겠네요.

 

결과

1+2=3

 

boolean startsWith(String prefix)

boolean startsWith(String prefix, int toffset)

boolean endsWith(String suffix)

startsWith는 문자열이 prefix로 시작하는지 확인하는 메소드입니다. prefix의 비교 위치를 지정하려면 startsWith의 두번째 메소드를 사용하면 됩니다.

endsWith는 문자열이 suffix의 문자열로 끝나는지 확인하는 메소드입니다. suffix로 끝난다면 true, 다르게 끝나면 false를 반환합니다.

		
String str="ABCDEF";
System.out.println(str.startsWith("AB"));
System.out.println(str.startsWith("BC",1));
System.out.println(str.endsWith("EF"));
System.out.println(str.endsWith("EE"));

결과

true
true
true
false

 

 

 

 

 

String replace(char oldChar, char newChar)

String replace(CharSequence target, CharSequence replacement)

String replaceAll(String regex, String replacement)

String replaceFirst(String regex, String replacement)

문자나 문자열을 다른 문자나 문자열로 바꾸고 싶을때 사용하는 메소드입니다. 메소드 이름이 직관적이기 때문에 사용법을 익히는데 그리 어렵지 않을 겁니다. 또는 아래의 예를 보고 이해하면 되겠지요.

String str="AB-----AB-----AB";
String str1=str.replaceFirst("AB", "ab");
String str2=str.replace("AB", "ab");
String str3=str.replaceAll("AB", "ab");
String str4=str.replace('A', 'a');
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
System.out.println(str4);

 

결과

ab-----AB-----AB
ab-----ab-----ab
ab-----ab-----ab
aB-----aB-----aB

 

사실 이 중에서도 자주 쓰이는거 외에는 별로 쓸 일이 없을 겁니다. 자주 필요한 몇가지만 기억해두면 될 것 같네요. 다음 포스팅에서도 String 메소드에 대해서 더 알아보도록 합시다.

반응형
블로그 이미지

REAKWON

와나진짜

,

14500번 주사위 굴리기

 

https://www.acmicpc.net/problem/14500

 

14500번: 테트로미노

폴리오미노란 크기가 1×1인 정사각형을 여러 개 이어서 붙인 도형이며, 다음과 같은 조건을 만족해야 한다. 정사각형은 서로 겹치면 안된다. 도형은 모두 연결되어 있어야 한다. 정사각형의 꼭짓점끼리 연결되어 있어야 한다. 즉, 변과 꼭짓점이 맞닿아있으면 안된다. 정사각형 4개를 이어 붙인 폴리오미노는 테트로미노라고 하며, 다음과 같은 5가지가 있다. 아름이는 크기가 N×M인 종이 위에 테트로미노 하나를 놓으려고 한다. 종이는 1×1 크기의 칸으로 나누어져

www.acmicpc.net

 

문제 해석


테트리스 게임을 조금 활용한 문제같습니다. 문제는 간단합니다.

어떤 판에 숫자들이 적혀있는데 여기서 테트리스모양으로 숫자를 골라 가장 최대가 나오는 값을 구하는 문제입니다.

테트리스 모양은 여러분들이 잘 아시죠? 여기서 물론 테트리스의 블록들은 4방향 회전이 가능합니다.

 

예제를 직접 풀어보도록 하지요. 아래의 문제의 첫번째 예제가 주어졌습니다.

1 2 3 4 5
5 4 3 2 1
2 3 4 5 6
6 5 4 3 2
1 2 1 2 1

여기서 가장 큰 수를 고르면 파랑색의 숫자들이 되겠습니다. 이 외에 더 큰 값을 테트리스 모양대로 고를수가 없는 것을 알 수 있습니다.

 

 

제약 조건


세로와 가로의 길이(N,M)은 4이상 500이하로 주어집니다. 입력으로 주어지는 수는 1000을 넘지 않는다고 합니다.

 

풀이


이 문제를 이전에 JAVA로 풀어놓은게 있으므로 이번 포스팅은 JAVA 코드로 구현했습니다.

 

가로와 세로의 길이는 최대 500이며, 숫자들은 모두 500x500으로 250,000개가 되겠네요. 또 이 중 최대 4개의 숫자만 더하면 되니 문제의 답은 총 4000을 초과하지 않습니다.

이와 같이 문제의 값의 범위가 적을때는 brute-force(전수조사)로 그 문제의 해답을 찾을 수 있습니다. 모든 가능한 수를 구하는 것입니다.

문제를 풀기에 앞서 손으로 테트리스 모양을 만듭시다. 총 7개의 모양을 가지고 있고, 4방향으로 회전을 하기 때문에 대략 28개의 테트리스 모양을 배열로 만들겁니다.

1은 블록이 있는 공간을 말하고 0은 블록이 없는 공간을 의미합니다. 이해가 더 잘가기 위해 아래 세가지 테트리스 블록을 그림으로 첨부했습니다.

 

 

 

이제 이것을 배열로 만드는 것이죠. 이렇게 말이죠. 이 정도 약간의 노가다는 해도 괜찮아요. 시간이 그렇게 많지 않으니까요.

 

 


static int [][][]shapes={
		{{1,1,1,1}},{{1},{1},{1},{1}}, 
		{{0,1,0},{1,1,1}},{{1,1,1},{0,1,0}},{{0,1},{1,1},{0,1}},{{1,0},{1,1},{1,0}},
		{{1,1,1},{1,0,0}},{{0,0,1},{1,1,1}},{{1,1},{0,1},{0,1}},{{1,0},{1,0},{1,1}},
		{{1,1,1},{0,0,1}},{{1,0,0},{1,1,1}},{{0,1},{0,1},{1,1}},{{1,1},{1,0},{1,0}},
		{{1,1},{1,1}},
		{{1,0},{1,1},{0,1}},{{0,1,1},{1,1,0}},
		{{0,1},{1,1},{1,0}},{{1,1,0},{0,1,1}}
};

 

배열 첫번째 모양은 일자모양(l)이고, 두번째는 ㅗ모양입니다. 세번째 모양은 L자 모양이 되겠습니다. 이것을 사방으로 회전하다 보니까 2차원 배열이 한 줄에 최대 4개까지 나오며, 일자모양과 정사각형 모양 등 네 방향으로 회전할때 같은 모양이 나올 경우는 4개까지 나오지 않지요.

 

실제 문제를 푸는 코드는 아래에 있습니다. 

static int maxValue(int y,int x){
		int max=0;
		for(int next=0;next<19;next++){
			int r=shapes[next].length;
			int c=shapes[next][0].length;
			if(y+r>n||x+c>m) continue;
			int sum=0;
			for(int i=y;i<y+r;i++) for(int j=x;j<x+c;j++)
				sum+=map[i][j]*shapes[next][i-y][j-x];
			max=Math.max(max, sum);
		}
		return max;
	}

 

인자로 받은 y와 x는 비교할 위치를 말합니다. y는 세로축, x는 가로축의 좌표라고 보면 됩니다.

 

우리는 회전해서 나온 각각의 모양 19개(3차원 배열속의 2차원 배열 갯수)를 위에서 만들었지 않았나요? 그러니 for문을 19번 반복을 하는 겁니다.

이 for문 안에 r과 c는 만들었던 모양의 가로 길이, 세로 길이를 나타냅니다. 만약 {{ 0,1,0 }, { 1,1,1 }}의 (ㅗ)모양이라면 r의 값은 2, c의 값은 3이 되는 겁니다. 이런 길이가 입력 받은 보드의 범위를 넘어가게 되면 구할 필요가 없어지지요.

그런 조건을 if문으로 거릅니다.

int r=shapes[next].length;
int c=shapes[next][0].length;
if(y+r>n||x+c>m) continue;

 

범위 안에 있다면 이제 더해주면 됩니다. 우리가 블록이 있는 경우에는 1을 저장했었죠? 블록이 없는 경우는 0을 저장했었습니다. 그래서 판의 숫자에 이 이차원 배열을 단지 곱해주면 됩니다. 이 값을 모두 더하는 것이죠.

sum+=map[i][j]*shapes[next][i-y][j-x];

 

 

이제 모든 판의 좌표를 돌면서 가장 큰 값을 구하면 그게 답이 되는 겁니다. 전체 코드는 아래에 있습니다.

 

import java.util.Scanner;
public class Main {

	static int n,m;
	static int [][]map;
	static int [][][]shapes={
			{{1,1,1,1}},{{1},{1},{1},{1}}, 
			{{0,1,0},{1,1,1}},{{1,1,1},{0,1,0}},{{0,1},{1,1},{0,1}},{{1,0},{1,1},{1,0}},
			{{1,1,1},{1,0,0}},{{0,0,1},{1,1,1}},{{1,1},{0,1},{0,1}},{{1,0},{1,0},{1,1}},
			{{1,1,1},{0,0,1}},{{1,0,0},{1,1,1}},{{0,1},{0,1},{1,1}},{{1,1},{1,0},{1,0}},
			{{1,1},{1,1}},
			{{1,0},{1,1},{0,1}},{{0,1,1},{1,1,0}},
			{{0,1},{1,1},{1,0}},{{1,1,0},{0,1,1}}
	};
	public static void main(String[] ar){
		Scanner in=new Scanner(System.in);
		n=in.nextInt(); m=in.nextInt();
		map=new int[n][m];
		for(int i=0;i<n;i++) for(int j=0;j<m;j++)
			map[i][j]=in.nextInt();
		int max=0;
		for(int i=0;i<n;i++) for(int j=0;j<m;j++)
				max=Math.max(maxValue(i,j), max);
		System.out.println(max);
	}
	static int maxValue(int y,int x){
		int max=0;
		for(int next=0;next<19;next++){
			int r=shapes[next].length;
			int c=shapes[next][0].length;
			if(y+r>n||x+c>m) continue;
			int sum=0;
			for(int i=y;i<y+r;i++) for(int j=x;j<x+c;j++)
					sum+=map[i][j]*shapes[next][i-y][j-x];
			max=Math.max(max, sum);
		}
		return max;
	}
}

 

이상으로 테트로미노 문제를 풀어보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,