큐와 스택, 데크

LeeJE20·2021년 3월 10일
0

알고리즘 공부

목록 보기
1/4

프로그래밍 대회에서 배우는 알고리즘 문제해결 전략 책 내용 정리입니다.

19.1 도입

  • 일렬로 늘어선 자료들
  • 특정 순서로 넣고 특정 순서로 꺼낼 수 있음

큐 queue

  • 가장 먼저 들어간 자료를 가장 먼저 꺼낸다.
  • 선입선출 (FIFO, First In First Out)
  • ex) 놀이공원 줄

스택 stack

  • 한 쪽 끝에서만 자료를 넣고 뺀다 : 가장 늦게 들어간 자료를 가장 먼저 꺼낸다
  • 후입선출 (LIFO, Last In First Out)
  • 컴퓨터는 내부적으로 스택을 사용해 함수들의 문맥 관리

데크 dequeue

  • 양쪽 끝에서 자료를 넣고 뺄 수 있음

작업

  • push : 자료를 넣는 작업
  • pop : 자료를 꺼내는 작업

O(1)O(1)에 이루어져야 함

19.2 큐와 스택, 데크의 구현

연결 리스트

  • 장점: 모든 연산이 상수 시간이어야 한다는 요구 조건 쉽게 충족
  • 단점: 노드의 할당, 삭제, 포인터를 따라가는데 시간 소요

동적 배열

  • 스택 : 그냥 하면 됨
  • 큐, 덱
    • 배열의 맨 앞에서 원소를 삭제하려면 O(n)O(n)
    • 환형 버퍼 (circular buffer)
      • 처음, 마지막 원소의 위치를 head, tail에 유지
      • 맨 앞에서 원소를 꺼낼 때, head를 다음 원소로 옮김
    cf) 실제 대부분은 tail이 마지막 원소 다음 위치: 텅 빈 큐 구현

표준 라이브러리

이미 구현되어 있다!

19.3 스택과 큐의 활용

큐: 조세푸스 문제

죽을 사람을 가리키는 포인터를 옮기는 대신, 사람들을 움직인다.

  1. 큐의 첫 번째 사람이 나와서 죽고
  2. 큐의 맨 앞에 있는 사람을 맨 뒤로 보내는 작업을 k -1 번 반복

스택: 울타리 자르기 문제 (FENCE)

분할정복: O(NlogN)O(NlogN)

스택: O(N)O(N)

최대 사각형: 어떤 판자를 완전히 포함하는 사각형 중 면적이 최대인 사각형

특징:

  • 이 사각형의 높이는 i번 판자와 항상 같다
  • 이 사각형의 왼쪽 끝과 오른쪽 끝은 i번 판자보다 낮다 → left[i], right[i]

최대 사각형의 넓이: (right[i] - left[i] - 1) * h[i]

left[i], right[i]를 찾는 알고리즘은 판자의 개수에 비례하는 시간이 걸림

전체 알고리즘은 O(N2)O(N^2)이 걸림!

모든 판자에 대해 left[i], right[i]를 상수 시간에 찾는 법?

→ 다른 판자에 대해 계산했던 정보 재활용!

스위핑 알고리즘의 설계

맨 왼쪽 0번 판자부터 순서대로 각 판자를 처리.

  • i번째 판자가 맨 왼쪽/오른쪽에 있을 경우에 대비해 입력으로 주어진 판자들의 양쪽 끝에 높이 0인 판자 첨가

0번 판자

left[0] = -1

right[0] : 지금 계산할 방법 없음

1번 판자

  • h[0] > h[1] 인 경우 (1번 판자의 높이가 낮다)

    right[0] = 1

    → 0번 판자의 최대 사각형 찾음

    이후 0번 판자는 더이상 의미가 없음

    • 1번 판자가 더 낮음 → 이후의 어떤 판자 i에 대해서도 left[i] = 0이 될 일이 없음

      → 0번 판자 지움

      왼쪽에는 아무런 판자도 없음

      → left[1] = -1

  • h[0] < h[1] 인 경우 (1번 판자의 높이 높다)

    이전 판자가 left임..

    left[1] = 0

    right[0], right[1]은 알 수 없음

  • h[0] == h[1] 인 경우 (두 판자의 높이가 같다)

    두 판자의 최대 사각형은 완전히 똑같음

    → 0번 판자를 남겨둘 이유 없음

    → h[0] > h[1]인 경우와 똑같이 처리


일반화

i번 판자..

왼쪽에 자신보다 높은 판자들이 남아 있다면, 그들의 최대 사각형을 자신이 막고 있음

→ 이들 전부에 대해 최대 사각형의 넓이를 계산하고 이들을 지워버림

결과: i번 판자 왼쪽에는 자신보다 낮은 판자만 있음 → left[i]를 항상 알 수 있음

// 각 판자의 높이를 저장하는 배 열 
vector<int> h;
// 스택을 사용한 o (n) 해법 
int solveStack() 
{
	// 남아 있는 판자들의 위치들을 저장한다. 
	stack<int> remaining;
	h.push_back(0);
	int ret = 0;
	for (int i = 0; i < h.size() ; ++i) 
	{
		// 남아 있는 판자들 중 오른쪽 끝 판자가 h[il보다 높다면 
		// 이 판자의 최대 사각형은 i에서 끝난다.
		while ( !remaining.empty() && h[remaining.top()] >= h [ i ] ) 
		{
			int j = remaining.top();
			remaining.pop();
			int width = -1;
			// j번째 판자 왼쪽에 판자가 하나도 안 남아 있는 경우 left[j]=-l ,
			// 아닌 경우 left(j]=남아 있는 판자 중 가장 오른쪽에 있는 판자의 번호 
			// 가 된다.
			if ( remaining.empty())
				width = i;
			else
				width = ( i - remaining.top() - 1 );
			ret = max(ret, h[j] * width);
		}
		remaining.push(i);
	}
	return ret;
}

시간복잡도

  • while 문 내부가 몇 번 반복되는가?

    while문이 한 번 수행될 때마다 remaining에서 원소가 하나 빠짐

    remaining에는 정확히 N개의 원소가 들어감

    → while문의 총 수행 횟수는 O(N)O(N)

O(N)O(N)으로 문제 해결 가능

19.4 문제: 짝이 맞지 않는 괄호 (BRACKETS2)

여는 괄호: 스택에 push

닫는 괄호: 스택 맨 위의 괄호와 맞춰봄

→ 안 맞으면 오류

💡실수!
1. 스택에서 여는 괄호 꺼내려고 하는데, 스택이 비어 있는 경우
2. 마지막에 열린 괄호가 남아 있는지 확인

💡꿀팁!
여는 괄호/닫는 괄호의 목록을 문자열에 저장
→ 문자가 여는 괄호인지/짝이 맞는지 쉽게 확인

bool wellMatched(const string& formula) 
{
	// 여는 괄호 문자들과 닫는 괄호 문자들
	const string opening("({["), closing(")}]");
	// 이미 열린 괄호들을 순서대로 담는 스택
	stack<char> openStack;
	for(int i = 0; i < formula.size(); ++i)
		// 여는 괄호인지 닫는 괄호인지 확인한다 
		if(opening.find(formula[i]) != -1)
			// 여는 괄호라면 무조건 스택 에 집어넣는다. 
			openStack.push(formula[i]); 
		else 
		{
			// 이 외의 경우 스택 맨 위의 문자와 맞춰보자.
			// 스택이 비어 있는 경우에는 실패 
			if(openStack.empty()) return false;
			// 서로 짝이 맞지 않아도 실패
			if(opening.find(openStack.top()) != closing.find(formula[i])) 
			return false;
			// 짝을 맞춘 괄호는 스택에서 뺀다. 
			openStack.pop();
		}
	// 닫히지 않은 괄호가 없어야 성공 
	return openStack.empty();
}

19.6 문제: 외계 신호 분석 (ITES)

수열이 주어질 때, head와 tail 사이 구간을 검사한다.

구간합이 K 이상이 되었을 경우, 더 이상 구간의 길이를 늘려봐야 답을 찾을 수 없다.

종료하기 전에 마지막으로 검사한 구간.. head가 정해져 있을 때 이 구간 외의 구간은 답이 될 수 없다. 후보 구간이라 부르다.

head가 늘어났을 때 tail이 줄어드는 일은 없다.

head가 증가했는데, tail이 감소 했다면 이 후보 구간은 이전 후보 구간의 일부가 된다.

이 구간의 합이 이미 K 이상이라면 이전 후보 구간은 더 일찍 끝났어야 하므로, 이런 경우는 있을 수 없다.

⇒ 후보 구간의 tail을 찾을 때 head에서 시작하지 말고 마지막에 찾았던 tail에서부터 시작하자!

head가 증가하고 나면 지나쳐온 숫자들은 고려할 필요 없음: 후보 구간에 포함된 숫자들만 저장하면 됨

//코드 19.4 외계 신호 분석 문제를 해결하는 최적화된 알고리즘
int optimized(const vector<int>& signals, int k) { 
int ret = 0, tail = 0, rangeSum = signals[0]; 
for(int head = 0; head < signals.sizeO; ++head) {
	// rangeSum이 k 이상인 최초의 구간을 만날 때까지 tail을 옮긴다. 
	while(rangeSum < k && tail + 1 < signals.size《)) 
	rangeSum += s gnals[++tail];
	f(rangeSum = k) ret++;
	// signalsthead]는 이제 구간에서 빠진다. 
	rangeSum -= signals[head];
}
return ret;

시간복잡도

중첩 반복문..

while문의 내부가 실행될 때마다 tail은 증가

tail은 0부터 N까지 증가하므로, while문의 내부는 최대 N번 수행됨.

분할 상환 분석을 이용하면 코드 19.4의 시간 복잡도는 O(N)O(N)이다.

0개의 댓글

관련 채용 정보