Deloitte의 프로젝트 인턴에 지원했었다. 컨설팅 회사인 만큼, 학벌도 많이 볼 것이라 생각했고 지원자도 많을 것 같아 크게 기대는 안했었다. 여기도 인턴 공고가 꽤 많이 올라오는 것 같던데.. 몇 번 자기소개서를 쓰다가 어차피 안 될 것 같다는 생각에 관 둔 적이 많았다.
지원 공고
그러다 처음으로 자기소개서를 완성해서 제출을 했었는데, 기대는 별로 안했었다. 특히 응시 자격 중 저 'Fluent in English'가 굉장히 마음에 걸렸다. (필자는 토익 800점대 중반에 오픽 IM1의 소유자로, 영어에 소질이 있는 편은 아니었다. 해외 경험도 거의 전무하다 싶이 하다.) 오픽은 기재하지 않고, 토익 점수만 기재해서 '나 토익 800점 대인데 거르려면 걸러라' 라는 생각으로 지원했다.
딜로이트 인턴은 어떤 공고든 자기소개서 문항이 똑같은 것 같은데..지원동기와 최종 커리어목표 두 가지 문항이었고, 모두 1000자였다. 경험 위주로 작성했었다.
서류 합격 메일
합격 메일을 받고 굉장히 신기했다. 또한 놀랐던 것은, 마감일 바로 하루 뒤에 결과가 나왔던 것이다. 물리적으로 이력서를 전부 검수할 수 있는 시간인지 모르겠다.. 내 생각에는 지원서가 들어올 때마다 그때 그때 적당히 확인하고, 거의 마감 되자마자 합격 메일을 보내는 것이 아닌가 싶다.
추가로, 25일과 26일 모두 참석이 가능하다고 회신을 보냈는데.. 갑자기 내일 면접이 가능하냐고 물어왔다. 굉장히 인력이 급한 상황인 것 같았다. 그리고 바로 다음날 면접을 보게 되었다. (인성검사는 정말 성격 관련 문항만 있었고, 형식적인 프로세스라는 느낌을 받았다.)
면접 안내 메일
회사 사옥이 아닌 공유 오피스에서 면접을 봤다. (프로젝트 때문에 공유 오피스에서 근무 중이라고 했다.) 면접 분위기 자체는 편하게 진행되었으나, 질문이 아주 쉬웠던 것은 아니었다. 자기소개서 기반 질문은 별로 없었으며, '이러한 경험을 해봤냐' 라는 질문이 주를 이루었다.
면접 질문으로는
자기소개
주변에서 본인을 어떻게 평가 하는지
협업 경험
업무 하다가 막히면 어떻게 대처하는지
HR 관련 경험이 있는지 (나는 Cloud 관련 경험은 자기소개에서 많이 어필해서 HR 경험 질문을 받은 것 같다. 만약 HR 관련 경험을 주로 어필했다면 Cloud 관련 경험을 물어보지 않았을까 싶다.)
어떤 기업의 특정 상황을 가정한 뒤, 기업에게 HR Cloud를 사용하라고 설득해보라는 내용
영어 수준이 어느정도 되는지
마지막으로 Deloitte 혹은 Consulting과 관련하여 궁금한 것이 있는지
등등이 있었다.
컨설팅 회사 면접은 처음이었는데, 컨설팅 회사에서 주로 물어본다고 하는 Why Deloitte나 Why Consulting 같은 질문은 없었다. (인턴이라 그런 걸까?) 또한 영어 질문이 있을까 두려웠지만.. 영어 수준이 어떠냐고 물어볼 뿐 영어로 질문을 하거나 영어로 대답하라는 요구는 없었다. 그런데 영어가 중요하긴 한 것 같았다. 영어 실력 관련 질문에서 영어 독해는 어느정도 가능하지만, 듣고 말하는 것, 작문에는 어려움이 있다고 솔직하게 대답했는데, 마지막에 주로 어떤 업무를 하냐고 물어보았을때 영어 회의를 듣고 회의록 작성하는 것이 중요하다고 하더라... 이때 떨어질 것을 직감했다. ^^
불합격 메일
결과는 일주일 안쪽으로 나왔던 것 같다. 인력이 굉장히 급해보였기에 처음엔 기대를 했지만... 면접 본 뒤 결과가 너무나도 예상이 갔었기 때문에 그다지 슬프진 않았다. 어찌 되었든 생각보다 컨설팅 인턴의 진입 문턱이 높은 것 같진 않으니, 해보고 싶은 분들이 있다면 겁 먹지 말고 적극적으로 지원해봐도 좋을 것 같다. (다만, 해당 프로젝트 관련 지식과 경험..그리고 영어실력(ㅠㅠ)은 합격을 위한 필수 요건인 듯하다.)
사실 해당 공고 이외에도, 네이버 인턴에 두 번 지원한 적이 있었으며 모두 서류에서 탈락했었다.
그럼에도 불구하고 세 번째 지원에서 서류 합격을 할 수 있었던 것을 보면 한 두번 서류에서 탈락했다고 일종의 블랙리스트...에 오르는 것은 아닌 것으로 보인다.
(꼭 여기서 인턴을 해보고 싶은 분이 있다면, 지원에 돈이 드는 것도 아니니 탈락을 너무 두려워하지 말고 적극적으로 지원해보라고 말씀드리고 싶다.)
자기소개서 문항은 1000자 짜리 세 개로, 분량이 꽤 있어서 쓰기 쉽지는 않았다.
[필수] 자기자신을 자유롭게 소개해 주세요. (1000자) > 전공 관련 내용, 지원 동기에 대해 작성했다.
[필수] 지원부문과 관련된 경험(스터디, 프로젝트, 업무 등)이 있으면 서술하고, 그 경험에서 자신이 어떤 역할을 했었고, 결과로 어떤 지식이나 교훈을 얻었는지 설명해 주세요. (1000자) > 리서치 인턴을 한 번 해 본 경험이 있어서, 그 경험에 대해 작성했다.
[필수] 본인이 보유한 Skill의 활용 정도를 모두 적어주세요. [작성 예시 : GIS 기술 및 툴 활용 역량, UX설계/기획 도구, 데이터 마이닝 관련 기술 역량 등 자유롭게 기술해주세요 ] (1000자) > OA역량...을 작성했다. 이거 때문에 떨어질 줄 알았다.
[필수] 협업 시 중요하게 생각하는 것과 잘하는 것을 설명해주세요. (커뮤니케이션 스킬, 친화력 있는 성격, 전문성 등) (1000자) > 인턴 당시 경험을 작성했다.
서류합격 메시지
사실 네이버 인턴의 경우 결과 발표 메시지만 보아도 합격 여부를 알 수 있다.
불합격의 경우 단순히 채용 결과가 발표 되었으니 메일을 확인하라고 메시지가 오지만, 합격에게는 Naver Careers(채용 홈페이지)의 메시지함을 확인하라고 문자가 오더라.
놀란 것은, 8월 2일에 지원을 완료했는데 8월 7일에 합격 메시지가 왔다.
지원자가 굉장히 많을 것 같은데... 일주일도 안되어서 서류 검토가 완료 되었다는게 놀라웠다. AI 필터링을 하는 걸까?
면접 준비의 흔적
이정도 규모 기업의 인턴에 서류 합격을 한 것은 처음이라, 신기하기도 해서 면접을 꽤 열심히 준비했었다.
크게 기업 분석, 최근 AI 기술 트렌드 조사, 경험 정리로 나누어 면접을 준비했다.
면접은 면접관 두 명과 피면접자 한 명으로 진행되었다.
기억에 남는 면접 질문으로는,
자기소개
최근 AI 트렌드
어떤 업무를 할 것이라 생각하고 지원하였는지
이번 인턴을 통해 어떤 것을 얻어가고 싶은지
IT 용어들 등 적응 빨리 한느 편인지
네이버 LLM이 타사 LLM에 비해 가지는 장점
네이버가 경쟁사들에 비해 차별화 되기 위하여 해야 할 것
마지막으로 하고 싶은 말
궁금한 점
이 있었으며, 이외에도 개인적인 경험에 대한 질문들도 많았다. (거의 30분 꽉 채워 진행되었었다.)
면접관 분께서 내가 이전에 했었던 인턴 업무가 네이버 인턴 업무와 굉장히 Fit 할 것 같다는 말씀을 하셔서, 합격할 줄 알았다. ^^
하반기 공채를 지원할 것인지에 대한 질문도 했었는데... 아마 여기서 대답을 제대로 못한게 패인이 아닐까 싶다.
너 탈락한거야
결과는 면접 본 지 일주일 정도 뒤에 나왔으며, 불합격이었다. (이제 메일 제목만 봐도 합불 여부를 알 수 있다!)
- 컴퓨터 장치들이 네트워크 상에서 서로를 인식하기 위해 사용하는 32bit 길이의 고유한 번호 - 0.0.0.0 ~ 255. 255. 255. 255 (0000 0000. 0000 0000. 0000 0000. 0000 0000 ~ 1111 1111. 1111 1111. 1111 1111. 1111 1111) 까지 존재할 수 있다. - 일부는 서브넷에 의하여, 일부는 호스트에 의하여 정해진다.
IP address의 class
- 네트워크상에 많은 IP주소를 사용해야 하면 Host Address 범위가 넓은 클래스를, 그렇지 않으면 Network 범위가 넓은 클래스를 사용
Class A: 0xxx xxxx. (1.0.0.0 - 126.0.0.0)
Class B: 10xx xxxx. (128.0.0.0 - 191.255.0.0)
Class C: 110x xxxx. (192.0.0.0 - 223.255.255.0)
- Class 마다 각각의 시작 숫자가 정해져있는 이유는 IP주소를 보고 편리하게 Class르 구분하기 위함 - 현재는 Class체계를 사용하지 않고 CIDR(Classless Inter-Domain Routing) 를 사용
2. NetMask
- IP에서 네트워크 주소와 호스트 주소를 분리하는마스크 - 네트워크 주소는 1로, 호스트 주소를 0으로 표기하여 IP주소와 AND 연산 하면 네트워크주소가 나온다.
2. SubnetMask
- 할당된 네트워크 주소를 다시 여러개의 작은 네트워크로 나누어 사용하는 것 - IP 주소 뒤에 "/x" 를 붙이는데, 이는 IP주소의 앞 x자리가 네트워크 주소라는 의미
라우터가 어떤 네트워크로 패킷을 보내야 하는지 판별하기 쉬움.
네트워크를 논리적으로 구분 할 수 있음.
IP주소를 효율적으로 관리 할 수 있음.
3. SubnetMask 계산 방법
ex ) 194. 139. 10. 7 / 25 - '194. 139. 10. 7' 는 '1100 0010. 1000 1011. 0000 1010. 0000 0111' 이다. (Class C IP주소로, 원래 마지막 뒤 8자리가 호스트 주소.) - '/25'는 앞 25bit가 네트워크 주소로 사용된다는 의미이므로 '1100 0010. 1000 1011. 0000 1010 0000 0111' 로, 끝 7자가 호스트 주소가 되고, 네 번째 옥텟의 첫번째 비트가 Subnet number가 되었다. - Subnet number는 네트워크 주소로 소속이 되고, Subnet Number가 0일때와 1일때로 네트워크 주소의 할당 자리는 2배가 되지만, 호스트 주소의 할당 자리는 1/2배가 된다. - 첫 번째 Subnet 범위는 194. 139. 10. 0 ~ 194. 139. 10. 127, 두 번째 Subnet 범위는 194. 139. 10. 128 ~ 194. 139. 10. 255가 된다. - Subnetting을 통해 하나의 큰 네트워클르 더 작은 서브넷으로 분리하면 각 서브넷은 자체적인 네트워크 주소와 IP 주소 범위를 가지고, 서로 다른 서브넷간에는 라우터를 통하여 통신한다.
4. Network Address& BroadCast Address - Network Address : 특정 서브넷의 시작 주소로, 일반적으로 호스트에 할당되지 않으며 라우팅 테이블에서 경로를 지정할 때 사용 - Network Address는 호스트 부분의 모든 비트가 0인 주소로 표현된다. - Broadcast Address: 특정 서브넷 내의 모든 호스트에 패킷을 전송하기 위한 주소로, 일반적으로 호스트에 할당되지 않는다. - Broadcast Address는 호스트 부분의 모든 비트가 1인 주소로 표현된다.
- 기본적으로 컴퓨터는 하드웨어 위에 운영체제가 올라가있고, 그 위에서 응용 프로그램이 작동하는 방식으로 동작. 이 때 메모리에 상주하는 운영체제의 핵심 부분을 커널이라고 한다.
- 가상머신은 하나의 물리적인 컴퓨터에서 여러개의 독립적인 가상환경을 생성하여 동작시키는 기술
- 하이퍼바이저는 가상머신의 생성, 자원 할당, 네트워킹, 저장장치 관리, 보안 등을 처리하며, 가상머신들 간의 리소스 분할과 격리를 관리하여 각각의 가상머신이 독립적으로 동작 할 수 있도록 한다.
- 도커는 컨테이너 기술을 사용하여 호스트 시스템의 운영체제를 공유하기 때문에 더 가볍고 효율적인 리소스 사용이 가능
2) 목적
- 하나의 컴퓨터로 동시에 서로 다른 2개 이상의 운영체제를 실행하고 싶을 때
- 하나의 컴퓨터의 자원을 여러 명에게 나누어주고자 하는데 각 사용자 간 상호 간섭을 없애고 싶을 때
- 컴퓨터의 다른 영역에 영향을 주지 않는 독립적인 환경을 만들고 싶을 때
- 각 가상머신은 다른 가상머신과 격리되어 있으며, 서로의 운영체제와 애플리케이션에 영향을 주지 않는다. 또한 물리적인 하드웨어와 독립적이므로 다른 호스트 컴퓨터로 이동하거나 클라우드 환경에서 실행될 수 있다.
2. 파티션과 LVM(Logical Volume Manager)
파티션 : 물리적인 디스크를 여러 논리적인 섹션으로 분할하는 것. 일반적으로 운영체제, 응용 프로그램, 데이터 등을 구별하기 위해 사용
LVM : 물리적인 디스크를 논리적인 단위로 관리하는 프레임워크. 논리적인 디스크 공간을 필요에 따라 동적으로 변경하거나 재분배 할 수 있다.
파티션이 디스크를 분할하는데 초점을 맞춘 기법이라면, LVM은 디스크를 분할하고 합치고 크기를 동적으로 조정하는 등 더욱 유연한 디스크 관리를 가능하게 한다.
가상머신에서 파티션 분할
리눅스 시스템에서는 하나의 하드디스크에 최대 4개의 기본 파티션을 생성할 수 있으며, 필요에 따라 더 많은 파티션을 생성하고 싶을 때 확장 파티션을 생성한다. 즉, 4개 이상의 파티션이 필요하면 하나의 파티션을 확장 파티션으로 잡고 확장 파티션 내에서 다시 논리 파티션을 구분하는 방식으로 우회하는 것이다.
위 사진은 가상머신에서 리눅스 파일 시스템 계층 구조를 간략하게 구현한 것이다.
/
루트 디렉토리로, 시스템이 시작되는 첫 번째 위치
/boot
리눅스 시스템을 부팅하는데 필요한 파일들이 저장되어 있는 디렉토리
[SWAP]
컴퓨터의 RAM이 가득 찼을 때, 리눅스 시스템이 임시로 사용하는 디스크 공간
/home
일반 사용자들의 홈 디렉토리를 담고 있다. 사용자를 등록하면 일반적으로 /home/계정명으로 사용자 계정 디렉토리가 생성되고 운영된다.
/var
시스템이 실행되면서 내용이 자주 변경될 수 있는 데이터를 저장 (로그 - 시스템 상태, 동작 등을 기록 / 스풀 - 프린터 작업, 메일, 스케줄 작업 등)
/srv
시스템에서 제공하는 서비스 데이터를 저장
/tmp
시스템이 재부팅 될 때 삭제되는 일시적인 파일들을 저장
3.Sudo
- 'super user do'의 약자로, 각종 명령어 앞에 sudo를 붙이면 최고 관리자 권한으로 실행된다.
- etc/sudoer 설정 파일에 명시된 사용자만 사용 가능하다.
비밀번호 정책설정
- Deafult env_reset : sudo를 사용할 때 사용자가 시스템의 환경변수만 사용되게 하도록 제한. 환경변수가 예기치 않게 변경되어 의도하지 않은 동작을 유발 하지 않도록 함.
- mail_badpass : 비밀번호를 잘못 입력한 경우, 이 설정에 따라 알림 이메일이 관리자에게 전송.
- secure_path : sudo를 사용하여 실행할 수 있는 명령어의 경로를 제한. 이를 통해 sudo를 사용하는 사용자가 임의의 명령어를 실행하는 것을 방지.
- authfail_message : 비밀번호 인증에 실패한 경우에 표시되는 메시지를 지정. %d는 실패한 비밀번호 시도 횟수를 나타내는 특수한 플레이스 홀더.
- badpass_message : 잘못된 비밀번호를 입력했을 때 나타나는 오류 메시지.
- log input/log output : sudo를 사용하여 실행한 명령어의 입력과 출력을 로그 파일에 기록
- requiretty : sudo를 사용하여 실행되는 명령이 tty(터미널)가 필요로 하는지를 지정
- iolog_dir : sudo 사용 로그파일의 디렉터리 경로를 지정
- passwd_tries : 사용자가 비밀번호를 재시도 할 수 있는 최대 횟수를 지정
권한 설정
해당 항목은 사용자별로 sudo를 사용할 수 있는 특권을 지정하는 부분으로, 각 줄의 구조는 "사용자명 호스트 = (실행할 사용자 : 실행할 그룹) 실행할 명령"을 의미한다.
따라서 위의 경우 모든 호스트(컴퓨터)에서 모든 사용자/모든 그룹으로 어떤 명령이든 실행 할 수 있도록 권한을 부여한 것이다.
sudo 그룹에 특정 사용자 추가하기
특정 사용자에게 sudo 권한을 부여하려면 '/etc/sudoers' 파일에서 권한 부여를 해주고, sudo group에 추가까지 마쳐야 한다.'
4. UFW (Uncomplicated Fire Wall)
- iptable 기반의 간단하고 사용하기 쉬운 방화벽 설정 도구
- 특정 포트, IP 주소 또는 서비스에 대한 접근을 허용하거나 거부하는 규칙을 설정할 수 있음
- 허용되거나 거부된 트래픽에 대한 로그를 기록
ufw를 통해 특정 포트만 열어 둘 수 있다.
- 포트는 컴퓨터와 외부 네트워크간의 통신을 위한 창구로 작동하므로, 특정 포트만 허용함으로써 악의적인 사용자들이 해당 포트를 통해 시스템에 엑세스하거나 공격하는 것을 방지 할 수 있음.
- 트래픽 제어 가능 (예컨대 웹 서버를 운영하는 경우 80번 포트만 허용하여 웹 트래픽을 처리하고, 다른 포트는 차단함으로써 비웹 트래픽을 제한).
5. SSH (Secure Shell)
- 네트워크상에서 안전한 원격 접속을 제공하는 프로토콜
- ssh는 데이터 통신을 암호화해서 보호하며, 무결성을 위해 암호화화 함께 메시지 무결성 체크를 사용, 공개키 기반 인증을 사용.
- ssh 나오기 전 사용하기 전 사용하던 telnet의 경우 평문으로 통신하며 무결성 체크를 지원하지 않고, 패스워드 인증을 사용.
/etc/ssh/sshd_config
- ssh 포트를 22번에서 4242번으로 변경 : 많은 공격자들이 기본적으로 사용되는 22번 포트를 대상으로 공격을 시도하므로, 포트 번호를 변경함으로써 보안을 강화
- ssh를 통해 root 권한으로 직접 접속하지 못하게 제한 : root는 시스템에서 최고 관리자 권한을 가지고 있으므로 일반 사용자에 대한 공격보다 훨씬 위험하기 때문에 root로의 직접적인 ssh 접속을 차단
6. 비밀번호 정책설정
/etc/login.defs
- 암호는 30일마다 만료
- 비밀번호를 수정하기 전에 허용되는 최소 일 수는 2
- 암호가 만료되기 7일 전에 경고 메시지를 전송
/etc/pam.d/common-password
- 최대 시도 횟수 : 3회
- 최소 길이 : 10
- 기존 암호와 달라야 하는 문자의 개수 : 7
- 대문자 1개 이상 포함, 소문자 1개 이상 포함, 숫자 1개 이상 포함
- 비밀번호에 유저명이 포함되어서는 안 됨
- 루트 계정에도 해당 규칙 적용
- 같은 문자가 3번 이상 반복되면 안 됨
* pam_pwquality
- pam(Pluggable Authentication Modules) : Linux 시스템에서 인증과 관련된 작업을 처리하는데 사용되는 프레임워크
pam을 사용하기 이전 리눅스 시스템에서는 각 응용 프로그램이 사용자 인증을 위해 자체적으로 로직을 구현하여 사용했기 때문에, 사용자 정보가 담긴 주요 시스템 파일인 '/etc/passwd'에 대한 접근 권한을 가지고 있어야 했다. 그러나 응용 프로그램이 사용자 정보가 담긴 주요 시스템 파일에 대한 직접적인 접근 권한을 가지고 있을 경우 보안과 관련한 위협이 있기 떄문에 pam이 등장하게 되었다. pam을 사용하면 인증이 필요한 응용 프로그램은 더 이상 passwd 파일을 열람하지 않고 pam 모듈에 사용자 인증을 요청하며, pam은 요청한 사용자의 정보를 가지고 결과를 도출하여 응용 프로그램에 전달한다.
7. CRON
Unix 및 Unix 기반 시스템에서 주기적인 작업을 예약하기 위해 사용되는 시간 기반 작업 스케줄러. 'crontab' 파일을 사용하여 작업을 정의하며, 정의된 작업은 지정된 시간 간격에 따라 실행 된다.
crontab -e
- crontab -e 명령어는 crontab 파일을 편집하기 위해 사용되는 명령어.
- crontab -e 는 현재 사용자의 개인적인 crontab 파일을 편집하며, /etc/crontab 파일은 시스템 전체의 cron 작업을 관리.
힙의 대표적인 종류로는 최대힙, 최소힙이 있으며 최대힙은 키값이 큰 원소가 높은 우선순위를, 최소힙은 키값이 작은 원소가 높은 우선순위를 가진다.
최대힙 - 부모 노드의 원소가 자식 노드의 원소보다 크거나 같은 완전 이진트리
최소힙 - 부모 노드의 원소가 자식 노드의 원소보다 작거나 같은 완전 이진트리
힙에서 삽입 및 삭제의 시간 복잡도는 O(log2n)이다.
2. (최대)Heap 구현하기
구현할 내용은 아래와 같다.
heapifyUp(int i) - i번째 인덱스의 값을 부모 노드와 비교하여, 부모 노드의 값이 더 작으면 자리를 교환한다.
heapifyDwon(int i) - i번째 인덱스의 값을 자식 노드 두 개와 비교하여, 둘 중 큰 자식노드보다 i번째 인덱스 값이 더 작으면 자리를 교환한다.
insert(E x) - 힙의 적절한 자리에 입력받은 값 x를 입력한다.
delete() - 루트 노드에 있는 값을 삭제한다.
max() - 루트 노드에 있는 값을 리턴한다.
isEmpty() - 힙이 비어있으면 true, 그렇지 않으면 false를 리턴한다.
clear() - 힙의 모든 내용물을 비운다.
printAll() - 힙의 모든 내용물을 출력한다.
** 원래 힙에서는 맨 앞의 값에만 접근이 가능하나, 테스트 편의상 printAll() 메서드를 만들어 사용하였다.
1) 인터페이스
public interface heapInterface<E> {
public void insert (E x);
public void delete();
public E max();
public boolean isEmpty();
public void clear();
public void printAll();
}
2) 클래스
- 필드 영역
//필드 영역
private E[] item;
private int size;
item[] - 원소를 저장한다.
size - 배열에 저장된 원소의 개수를 저장한다.
- 생성자 영역
//생성자 영역
public heap(int arraySize) {
item = (E[]) new Comparable [arraySize];
size=0;
}
배열의 크기를 입력받아 해당 크기의 배열을 생성한다. size변수는 0으로 초기화한다.
- 메서드 영역
<heapifyUp 메서드 구현>
private void heapifyUp(int i) {
if (i<0 || i>=item.length) {
throw new IllegalArgumentException();
} else {
int parent = (i-1)/2;
if (((Comparable) item[parent]).compareTo(item[i])<0) {
E tmp = item[i];
item[i] = item[parent];
item[parent] = tmp;
heapifyUp(parent);
}
}
}
parent 변수에 입력받은 인덱스 노드의 부모 인덱스를 저장한다.
만약 입력받은 인덱스 노드의 값보다 부모 노드의 값이 작으면 값을 바꾼다.
부모 노드와 인덱스 노드의 값이 같거나, 부모 노드 값이 더 클 때까지 부모 노드에 대하여 해당 과정을 반복한다.
만약 입력 받은 인덱스가 범위를 벗어났다면 에러를 리턴한다.
<heapifyDown 메서드 구현>
private void heapifyDown(int i) {
int left = 2*i+1;
int right = 2*i+2;
int max = i;
if (i<0 || i>item.length) {
throw new IllegalArgumentException();
} else {
if (left <=size-1) {
if (((Comparable) item[max]).compareTo(item[left])<0) {
max = left;
}
if (((Comparable) item[max]).compareTo(item[right])<0) {
max = right;
}
if (max!=i) {
E tmp = item[i];
item[i] = item[max];
item[max] = tmp;
heapifyDown(max); }
}
}
}
left 변수에 입력받은 인덱스 노드의 왼쪽 자식 노드 인덱스, right 변수에 오른쪽 자식 노드 인덱스를 저장한다.
max 변수에 입력받은 인덱스를 저장한다.
left 변수가 범위를 벗어나지 않는 동안 아래 과정을 반복한다.
max 인덱스의 값과 left, right 인덱스 값을 비교하여 max 인덱스에 가장 큰 값의 인덱스가 들어오게 한다.
max의 값이 입력받은 값과 다르면(자식 노드중에 더 큰 값이 있다면) 입력 받은 인덱스 노드와 max 인덱스 노드의 자리를 바꾼다.
입력받은 인덱스가 범위를 벗어났다면 에러를 리턴한다.
<insert 메서드 구현>
@Override
public void insert(E x) {
if (size==item.length) {
throw new IllegalArgumentException();
} else {
item[size]=x;
heapifyUp(size);
size++;
}
}
힙의 끝에 입력받은 값을 추가한다.
heapifyUp을 통해 해당 노드가 적절한 자리를 찾게 한다.
size 변수를 1 증가시킨다.
만약 힙에 빈 자리가 없었다면 에러를 리턴한다.
<delete 메서드 구현>
@Override
public void delete() {
if (isEmpty()) {
throw new IllegalArgumentException();
} else {
item[0]=item[size-1];
size--;
heapifyDown(0);
}
}
힙의 맨 끝자리에 있던 노드를 루트 노드 자리로 옮긴다.
size 변수를 1 감소시킨다.
heapifyDown을 통해 루트 노드 자리의 노드가 적절한 자리를 찾게 한다.
만약 힙이 비어 있었다면 에러를 리턴한다.
<max 메서드 구현>
@Override
public E max() {
if (isEmpty()) {
throw new IllegalArgumentException();
} else {
return item[0];
}
}
힙이 비어 있다면 에러를 리턴하고, 그렇지 않다면 루트 노드의 값을 리턴한다.
<isEmpty 메서드 구현>
@Override
public boolean isEmpty() {
return (size==0);
}
size 변수가 0인지 여부를 리턴한다.
<clear 메서드 구현>
@Override
public void clear() {
item = (E[]) new Comparable [size];
size=0;
}
item 배열과 size 변수를 초기화한다.
<printAll 메서드 구현>
@Override
public void printAll() {
for (int i=0; i<size; i++) {
System.out.print(item[i]+" ");
}
인덱스 0부터 배열 끝까지 순회하며 해당 인덱스의 값을 출력한다.
-전체 코드
public class heap<E> implements heapInterface<E> {
//필드 영역
private E[] item;
private int size;
//생성자 영역
public heap(int arraySize) {
item = (E[]) new Comparable [arraySize];
size=0;
}
//메서드 영역
private void heapifyUp(int i) {
if (i<0 || i>=item.length) {
throw new IllegalArgumentException();
} else {
int parent = (i-1)/2;
if (((Comparable) item[parent]).compareTo(item[i])<0) {
E tmp = item[i];
item[i] = item[parent];
item[parent] = tmp;
heapifyUp(parent);
}
}
}
private void heapifyDown(int i) {
int left = 2*i+1;
int right = 2*i+2;
int max = i;
if (i<0 || i>item.length) {
throw new IllegalArgumentException();
} else {
if (left <=size-1) {
if (((Comparable) item[max]).compareTo(item[left])<0) {
max = left;
}
if (((Comparable) item[max]).compareTo(item[right])<0) {
max = right;
}
if (max!=i) {
E tmp = item[i];
item[i] = item[max];
item[max] = tmp;
heapifyDown(max); }
}
}
}
@Override
public void insert(E x) {
if (size==item.length) {
throw new IllegalArgumentException();
} else {
item[size]=x;
heapifyUp(size);
size++;
}
}
@Override
public void delete() {
if (isEmpty()) {
throw new IllegalArgumentException();
} else {
item[0]=item[size-1];
size--;
heapifyDown(0);
}
}
@Override
public E max() {
if (isEmpty()) {
throw new IllegalArgumentException();
} else {
return item[0];
}
}
@Override
public boolean isEmpty() {
return (size==0);
}
@Override
public void clear() {
item = (E[]) new Comparable [size];
size=0;
}
@Override
public void printAll() {
for (int i=0; i<size; i++) {
System.out.print(item[i]+" ");
}
}
}
3) 결과 확인하기
- 코드
public class Test {
public static void main(String[] args) {
heap<Integer> lst = new heap<>(64);
lst.insert(100);
lst.insert(300);
lst.insert(400);
lst.insert(200);
lst.printAll();
System.out.println();
//100,200,300,400을 차례로 입력하고 출력합니다.
lst.delete();
lst.printAll();
System.out.println();
lst.delete();
lst.printAll();
System.out.println();
//루트에 해당하는 값을 삭제하고 출력합니다.
System.out.println(lst.isEmpty());
System.out.println(lst.max());
//lst가 비었는지 여부와 현재 루트에 있는 값을 출력합니다.
}
}
연결 리스트로 큐를 구현할 때는 tail에 큐의 마지막 노드를 저장하고 tail이 next로 큐의 첫 번째 값을 참조하게 한다.
2. Queue 구현하기
구현할 내용은 아래와 같다.
enqueue(E x) - 원소 x를 큐의 끝에 추가한다.
dequeue() - 큐의 가장 앞에 있는 원소를 삭제한다.
front() - 큐의 가장 앞에 있는 원소를 리턴한다.
isEmpty() - 큐가 비어있으면 true, 그렇지 않으면 false를 리턴한다.
dequeueAll() - 큐에 있는 모든 원소를 삭제한다.
printAll() - 큐의 모든 원소를 순서대로 출력한다.
** 원래 큐에서는 맨 앞의 값에만 접근이 가능하나, 테스트 편의상 printAll() 메서드를 만들어 사용하였다.
1) 노드 클래스
public class Node<E> {
public E item;
public Node<E> next;
public Node (E newItem) {
item = newItem;
next = null;
}
public Node (E newItem, Node<E> nextNode) {
item = newItem;
next = nextNode;
}
}
item 변수에 데이터를 저장한다.
next 변수에 다음 노드를 저장한다. next는 다음 노드의 위치를 가리키는 포인터가 된다.
첫 번째 생성자는 데이터를 입력받아 데이터를 item에 저장하고, 포인터 값은 null로 저장한다.
두 번째 생성자는 데이터와 다음 노드를 입력받아 데이터를 item에 저장하고, 포인터 값에 다음 노드를 저장한다.
2) 인터페이스
public interface QueueInterface<E> {
public void enqueue(E x);
public void dequeue();
public E front();
public boolean isEmpty();
public void dequeueAll();
public void printAll();
}
3) LinkedQueue 클래스
- 필드 & 생성자 영역
//필드 영역
private Node<E> tail;
int size=0;
//생성자 영역
public LinkedQueue() {
tail=null;
}
tail - 큐의 가장 끝 노드를 저장한다. (최근에 들어온 값)
size - 큐 원소의 개수를 저장한다.
생성자 - tail에 null 값을 할당한다.
** 사실 위 메서드 중 printAll을 구현하지 않는다면 size는 필요하지 않다.
- 메서드 영역
<enqueue 메서드 구현>
@Override
public void enqueue(E x) {
Node<E> newNode = new Node<>(x);
if (isEmpty()) {
tail = newNode;
tail.next=tail;
} else {
newNode.next=tail.next;
tail.next=newNode;
tail=newNode;
}
size++;
}
입력 받은 값 x를 가지는 새 노드를 생성한다.
만약 첫 노드라면 tail에 newNode를 입력하고 tail이 next로 스스로를 참조하게 한다.
그렇지 않다면 새 노드가 front를 참조하고, tail이 새 노드를 참조하게 한다.
tail 값에 newNode를 입력한다.
<dequeue 메서드 구현>
@Override
public void dequeue() {
if (isEmpty()) {
System.out.println("ERROR");
} else {
if (tail.next==tail) {
tail = null;
} else {
tail.next=tail.next.next;
}
}
size--;
}
만약 큐가 비어 있다면 에러를 출력한다.
만약 큐에 원소가 한 개라면 tail 값에 null을 입력한다.
큐에 원소가 여러 개라면 tail이 next로 기존 front값을 생략하고 그 다음 노드를 참조하게 한다.
<front 메서드 구현>
@Override
public E front() {
if (isEmpty()) {
return null;
} else {
return tail.next.item;
}
}
만약 큐가 비어 있다면 null을 리턴한다.
그렇지 않다면 front의 item값을 출력한다.
<isEmpty 메서드 구현>
@Override
public boolean isEmpty() {
return (tail==null);
}
tail==null 이면 true, 아니면 false를 리턴한다.
<dequeueAll 메서드 구현>
@Override
public void dequeueAll() {
tail=null;
size=0;
}
tail과 size를 초기화한다.
<printAll 메서드 구현>
@Override
public void printAll() {
Node<E> currNode=tail;
for (int i=0; i<size; i++) {
currNode=currNode.next;
System.out.print(currNode.item+" ");
}
}
front부터 size만큼 큐를 순회하며 해당 노드의 item값을 출력한다.
- 전체 코드
public class LinkedQueue<E> implements QueueInterface<E> {
//필드 영역
private Node<E> tail;
int size=0;
//생성자 영역
public LinkedQueue() {
tail=null;
}
//메서드 영역
@Override
public void enqueue(E x) {
Node<E> newNode = new Node<>(x);
if (isEmpty()) {
tail = newNode;
tail.next=tail;
} else {
newNode.next=tail.next;
tail.next=newNode;
tail=newNode;
}
size++;
}
@Override
public void dequeue() {
if (isEmpty()) {
System.out.println("ERROR");
} else {
if (tail.next==tail) {
tail = null;
} else {
tail.next=tail.next.next;
}
}
size--;
}
@Override
public E front() {
if (isEmpty()) {
return null;
} else {
return tail.next.item;
}
}
@Override
public boolean isEmpty() {
return (tail==null);
}
@Override
public void dequeueAll() {
tail=null;
size=0;
}
@Override
public void printAll() {
Node<E> currNode=tail;
for (int i=0; i<size; i++) {
currNode=currNode.next;
System.out.print(currNode.item+" ");
}
}
}
3) 결과 확인하기
<코드>
public class Test {
public static void main(String[] args) {
LinkedQueue<Integer> lst = new LinkedQueue<>();
lst.enqueue(100);
lst.enqueue(200);
lst.enqueue(300);
lst.enqueue(400);
lst.printAll();
System.out.println();
//LinkedQueue에 100,200,300,400을 순서대로 입력 후 현재 ArrayQueue 상태를 출력
System.out.println(lst.front());
//LinkedQueue의 가장 앞에 있는 값을 출력
lst.dequeue();
lst.printAll();
System.out.println();
//LinkedQueue에 100,200,300,400을 순서대로 입력 후 현재 ArrayQueue 상태를 출력
System.out.println(lst.isEmpty());
//LinkedQueue가 비었는지 출력
}
}