1. 문제
프로그래머스
코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.
programmers.co.kr
2. 풀이 과정
사악한 입력크기를 보니 효율성을 엄청 따져야 할 것 같은 문제라는 생각이 들었다. 게다가 이문제는 레벨3문제...
일단 저 표의 상태를 어떻게 저장할지를 고민했다.
그래도 일단 쉽게 생각했다.
- N 크기의 배열을 만들어 데이터가 있으면 True, 없으면 False
- 데이터가 삭제되면 True --> False
- 데이터가 복구되면 False --> True
- 삭제된 데이터를 저장할 저장소는 복구시 가장 최근의 삭제된 데이터를 불어와야 하므로 스택
- 모든 명령어를 다 끝낸 뒤 그 배열을 보고 True이면 O, False이면 F를 반환
이제 그림으로 보자
n:8 , k:2, cmd: ["D 2","C","U 3","C","D 4","C","U 2","Z","Z"]
0. 초기 상태
1. D 2
2. C
- 여기서 주의해야할 부분은 삭제 시 Cursor을 아래로 1 내려줘야 한다는 점이다.
3. U 3
- 여기서 주의해야할 부분은 5에서 위로 3칸 가면 2일것 같지만 4에 지금 데이터가 없으므로 이는 무시하고 움직여야 한다.
4. C
5. D 4
6. C
- 여기서 주의해야할 점은 7은 마지막 행이므로 삭제하게 되면 커서를 위로 한칸 올려줘야 한다.
7. U 2
8. Z
- 복구시에는 스택에서 하나 꺼내 해당 행을 다시 True로 만든다.
- 여기서 주의해야할 점은 복구시 커서는 변경되지 않는다.
9. Z
스택에는 4만이 남아있으므로 최종적으로 답은 "OOOOXOOO"가 된다.
이제 이를 구현해보자
3. 내 코드
import java.util.*;
class Solution {
public String solution(int n, int k, String[] cmd) {
int cursor = k; // 현재 선택된 행
int last_row = n - 1;
boolean[] table = new boolean[n]; // 표
Stack<Integer> delete_row = new Stack<>(); // 삭제 행 번호
Arrays.fill(table, true);
// 명령어 수행
for(String s: cmd) {
char c = s.charAt(0);
if(c == 'U') {
int move = Integer.parseInt(s.substring(2));
cursor = upCursor(table, cursor, move);
} else if(c == 'D') {
int move = Integer.parseInt(s.substring(2));
cursor = downCursor(table, cursor, move);
} else if(c == 'C') {
delete_row.push(cursor);
table[cursor] = false;
if(cursor == last_row) { // 삭제한 행의 위치가 마지막 행인 경우
last_row--;
cursor = upCursor(table, cursor, 1);
} else {
cursor = downCursor(table, cursor, 1);
}
} else if(c == 'Z') {
int refresh_row = delete_row.pop();
table[refresh_row] = true;
if(refresh_row == last_row + 1) { // 복구된 행의 위치가 마지막 행보다 아래인 경우
last_row++;
}
}
}
StringBuilder answer = new StringBuilder();
for(boolean haveData : table) {
// 표에 데이터가 있으면 O, 없으면 그대로 X
if(haveData) {
answer.append("O");
} else {
answer.append("X");
}
}
return answer.toString();
}
private int upCursor(boolean[] table, int cursor, int move) {
while(move > 0) {
cursor--;
if(table[cursor]) {
move--;
}
}
return cursor;
}
private int downCursor(boolean[] table, int cursor, int move) {
while(move > 0) {
cursor++;
if(table[cursor]) {
move--;
}
}
return cursor;
}
}
시간복잡도
- O(N^2)
채점 결과
정확성은 만족했지만 효율성은 만족하지 못했다...
입력크기와 시간복잡도를 고려하면 당연한 결과...
시간복잡도를 늘린부분은 다음일 것이다.
private int upCursor(boolean[] table, int cursor, int move) {
while(move > 0) {
cursor--;
if(table[cursor]) {
move--;
}
}
return cursor;
}
private int downCursor(boolean[] table, int cursor, int move) {
while(move > 0) {
cursor++;
if(table[cursor]) {
move--;
}
}
return cursor;
}
- 커서를 움직이는 부분
- 커서를 움직일 때 해당 데이터가 있는지 없는지를 따져야 했기 때문에 시간이 오래걸렸다.
그래서 이런거 따질필요도 없이 리스트를 사용해서 실제로 데이터를 삽입하고 삭제하는 방식으로도 구현해봤었다.
import java.util.*;
class Solution {
public String solution(int n, int k, String[] cmd) {
int cursor = k; // 현재 선택된 행
ArrayList<Integer> table = new ArrayList<>(); // 표
Stack<Integer> delete_row = new Stack<>(); // 삭제 행 번호
Stack<Integer> delete_data = new Stack<>(); // 삭제 데이터
for(int i = 0; i < n; i++) {
table.add(i); // 행 번호를 표에 저장
}
// 명령어 수행
for(String s: cmd) {
char c = s.charAt(0);
if(c == 'U') {
cursor -= Integer.parseInt(s.substring(2));
} else if(c == 'D') {
cursor += Integer.parseInt(s.substring(2));
} else if(c == 'C') {
delete_row.push(cursor);
delete_data.push(table.get(cursor));
table.remove(cursor);
if(cursor == table.size()) { // 삭제한 행의 위치가 마지막 행인 경우
cursor--;
}
} else if(c == 'Z') {
int row = delete_row.pop();
int data = delete_data.pop();
table.add(row, data);
if(row <= cursor) { // 복구된 데이터의 행이 현재 커서 위치보다 더 위일 경우
cursor++;
}
}
}
StringBuilder answer = new StringBuilder();
for(int i = 0; i < n; i++) {
answer.append("X"); // 정답 문자열 초기화
}
for(int row : table) {
// 표에 데이터가 있으면 O, 없으면 그대로 X
answer.setCharAt(row, 'O');
}
return answer.toString();
}
}
시간복잡도
- O(N^2)
채점 결과
결과가 더 안좋아졌다.
table.remove(cursor);
table.add(row, data);
- ArrayList에서 특정 인덱스 중간에 데이터를 삽입하고 특정 인덱스의 데이터를 삭제하는 작업은 생각보다 오래걸린다.
4. 다른 정답 코드와 비교
[프로그래머스 정답코드]
import java.util.Stack;
class Solution {
Stack<Node> stack;
public class Node {
int data;
Node prev, next;
public Node() {
}
public Node(int data) {
this.data = data;
this.prev = null;
this.next = null;
}
public Node remove() {
prev.next = next;
next.prev = prev;
if (this.next.data == -1) { // 마지막 노드인 경우 -> 삭제시 이전 노드로
return this.prev;
} else {
return this.next;
}
}
public void restore() { // 재연결
prev.next = this;
next.prev = this;
}
}
public Node init(int n) {
Node initNode = new Node(-1); // 시작 노드
Node prevNode = initNode;
Node curNode = null;
for (int i = 0; i < n; i++) {
curNode = new Node(i);
prevNode.next = curNode;
curNode.prev = prevNode;
prevNode = curNode;
}
Node endNode = new Node(-1);
curNode.next = endNode;
return initNode.next;
}
public String solution(int n, int k, String[] cmd) {
Node cursor = init(n);
stack = new Stack<>();
for (int i = 0; i < k; i++) { // 시작 커서 이동
cursor = cursor.next;
}
for (int i = 0; i < cmd.length; i++) {
String[] command = cmd[i].split(" ");
String op = command[0];
if ("U".equals(op)) {
int opNum = Integer.parseInt(command[1]);
cursor = up(cursor, opNum);
} else if ("D".equals(op)) {
int opNum = Integer.parseInt(command[1]);
cursor = down(cursor, opNum);
} else if ("C".equals(op)) {
cursor = delete(cursor);
} else { // 복구
restore();
}
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append("O");
}
while (!stack.isEmpty()) {
sb.setCharAt(stack.pop().data, 'X');
}
return sb.toString();
}
Node up(Node cur, int num) {
for (int i = 0; i < num; i++) {
cur = cur.prev;
}
return cur;
}
Node down(Node cur, int num) {
for (int i = 0; i < num; i++) {
cur = cur.next;
}
return cur;
}
Node delete(Node cur) {
stack.push(cur);
cur = cur.remove();
return cur;
}
void restore() {
Node dNode = stack.pop();
dNode.restore();
}
}
시간복잡도
- O(N)
설명
답은 Doubly LinkedList(양방향 링크드 리스트)였다.
Doubly LinkedList를 사용하게 되면 삭제, 삽입 시 처리를 효율적으로 할 수 있다.
Doubly LinkedList는 현재 노드에서 이전 노드와 다음 노드를 참조하고 있다.
삭제시
ex: Node(2) 삭제
- Node1의 다음 노드, Node3의 이전 노드에 대한 참조만 바꿔주면 된다
- 이러한 과정은 O(1)로 수행된다.
삽입시
- 삽입시에도 Node1의 다음노드, Node3의 이전 노드에 대한 참조만 다시 원래대로 바꿔주면 된다
- 이러한 과정은 O(1)로 수행된다.
5. 피드백
- 연속적인 데이터에 데이터를 특정부분에 삽입하거나 특정 부분의 데이터를 삭제할 때는 Doubly LinkedList가 매우 효율적이다.
- 그냥 삽입, 삭제하고 이전, 다음 노드에 대한 참조만 바꿔주면 됨.
- 최근 데이터를 참조하기 위해서는 스택 자료구조가 제일 효율적이다.
Doubly LinkedList 참고
자바 [JAVA] - Doubly LinkedList (이중 연결리스트) 구현하기
자료구조 관련 목록 링크 펼치기 더보기 0. 자바 컬렉션 프레임워크 (Java Collections Framework) 1. 리스트 인터페이스 (List Interface) 2. 어레이리스트 (ArrayList) 3. 단일 연결리스트 (Singly LinkedList) 4. 이중
st-lab.tistory.com
'코딩 테스트 > 프로그래머스' 카테고리의 다른 글
[프로그래머스] 기능 개발 (0) | 2024.05.04 |
---|---|
[프로그래머스] 요세푸스 문제 (0) | 2024.05.04 |
[프로그래머스] 크레인 인형뽑기 게임 (0) | 2024.05.02 |
[프로그래머스] 주식가격 (0) | 2024.05.01 |
[프로그래머스] 짝지어 제거하기 (0) | 2024.04.30 |