'파이썬 알고리즘 인터뷰(박상길, 책만)' 참조

해시 테이블

 

 

1. (문제) 중복 문자 없는 가장 긴 부분 문자열(리트코드 3)

- 문제: 중복 문자가 없는 가장 긴 부분 문자열(Substring)의 길이를 리턴하라

  - Input: s = "abcabcbb"
  - Output: 3
  - Explanation: The answer is "abc", with the length of 3.

문자열 내에서 중복이 없고, 길이가 가장 긴 부분 문자열을 출력하는 문제다.

s = ‘abcabcbb’ 일 때,

중복이 없고 가장 긴 문자열은 ‘abc’이므로,

부분문자열의 최대 길이는 3이다.

 

 

 

2. (풀이) 슬라이딩 윈도우와 투 포인터로 윈도우 크기 조정

image

우선, 문제의 풀이 과정을 그림으로 표현하겠다.

우선 투 포인터 left와 right를 모두 인덱스 0에 지정한다.

for 반복문으로 right 포인터를 한 칸씩 이동 시키면서, 부분 문자열의 중복 여부를 판단하고,

부분 문자열의 최대 길이를 업데이트한다.

위에 빨간선은 left와 right 포인터가 가리키는 범위(윈도우)이며,

윈도우 크기는 최대 3으로 ‘abc’, ‘bca’, ‘cab’ ‘abc’ 순서로 위치는 바뀌지만 크기는 모두 동일하다.

 

1) (해시 테이블) 생성 및 ‘key:value’ 삽입

우선, for 반복문으로 right 포인터를 오른쪽으로 한 칸씩 이동하면서 해시 테이블(used)에 포인터가 가리키는 값(char, 해시 맵 key)과 위치(right, 해시 맵 value)를 담는다.

# 해시 테이블 생성
used = {}

for right, char in enumerate(s):


        '''       

        # 해시테이블 상 현재 문자(key)에 인덱스(value) 삽입
        used[char] = right

 

2) (for 반복) 부분 문자열의 중복 여부 판단 and 최대 길이 업데이트

for right, char in enumerate(s):
            # 해시 테이블에 존재하는 문자의 경우 'left' 위치 업데이트
            # 'left' 위치가 업데이트 되는 경우 max_len 갱신 불필요
            if char in used and left <= used[char]:
                left = used[char] + 1

            else:
                # 문자의 최대 길이 업데이트
                max_len = max(max_len, right - left + 1)


            # 현재 문자에 인덱스 삽입
            used[char] = right

2)-1 for 반복문에서 right 포인터가 가리키는 문자열이 해시 테이블(used)에 포함 되어 있는지 여부를 판단한다.

만약 이미 해시 테이블에 포함된 문자열이라면 해당 윈도우 내에서 중복 문자열이 발생하기 때문이다.

따라서, left포인터를 ‘해시 테이블에 든 문자열의 위치 + 1 ‘로 이동시킨다면 윈도우 내에서 중복 문자가 제외된다.

2)-2 반면, for 반복문에서 right 포인터가 가리키는 문자열이 해시 테이블에 포함되어 있지 않다면,

해당 부분 문자열은 중복이 없으므로 길이를 구한다. 이전에 구한 최대 길이(max_len) 보다 더 크다면 새로 구한 길이로 ‘max_len’을 업데이트한다. for 반복 연산을 마친 후, ‘max_len’을 리턴하여 최대 길이를 출력하면 된다.

 

3. 전체 코드

class Solution(object):
    def lengthOfLongestSubstring(self, s):
        """
        :type s: str
        :rtype: int
        """

        # 해시 테이블 생성
        used = {}

        left = 0
        max_len = 0

        for right, char in enumerate(s):
            # 해시 테이블에 존재하는 문자의 경우 'left' 위치 업데이트
            # 'left' 위치가 업데이트 되는 경우 max_len 갱신 불필요
            if char in used and left <= used[char]:
                left = used[char] + 1

            else:
                # 문자의 최대 길이 업데이트
                max_len = max(max_len, right - left + 1)


            # 현재 문자에 인덱스 삽입
            used[char] = right

        return max_len

'파이썬 알고리즘 인터뷰(박상길, 책만) 참조'

문제 26. 원형 데크 디자인

1. 데크의 개념

데크(Deque) 자료형을 구현하는 문제다.

데크는 더블 엔디드 큐(Doble-Ended Queue)의 줄임말로, 양쪽 끝에서 큐 연산을 진행할 수 있는, 추상 자료형(ADT)이다.

데크는 이중 연결 리스트(Double Linked List)로 구현할 수 있다.

데크는 ‘head’와 ‘tail’ 두개의 포인터를 가지고 있으며,

새로운 아이템이 추가되면 앞쪽, 또는 뒤쪽으로 연결시켜주면 된다.

 

image

 

2. 문제 - 원형 데크 디자인(리트코드 641)

MyCircularDeque(int k) Initializes the deque with a maximum size of k.
    boolean insertFront() Adds an item at the front of Deque. Returns true if the operation is successful, or false otherwise.
    boolean insertLast() Adds an item at the rear of Deque. Returns true if the operation is successful, or false otherwise.
    boolean deleteFront() Deletes an item from the front of Deque. Returns true if the operation is successful, or false otherwise.
    boolean deleteLast() Deletes an item from the rear of Deque. Returns true if the operation is successful, or false otherwise.
    int getFront() Returns the front item from the Deque. Returns -1 if the deque is empty.
    int getRear() Returns the last item from Deque. Returns -1 if the deque is empty.
    boolean isEmpty() Returns true if the deque is empty, or false otherwise.
    boolean isFull() Returns true if the deque is full, or false otherwise.

위에서 제시한 데크의 함수를 구현하는 문제다

우선 데크의 맨 앞과 맨뒤에 요소를 추가하는 함수(insertFront() & insertLast())가 있다.

동시에 요소를 제거하는 함수(deleteFront() & deleteLast())도 구현한다.

요소의 추가 및 제거를 위해 별도의 함수(_add() & _del())를 구현할 예정이다.

데크의 맨 앞과 맨 뒤의 값을 조회하는 getFront(), getRear() 함수를 만들고,

마지막으로 데크가 비었는지 또는 가득 찼는지 여부를 판단하는 isEmpty(), isFull()함수를 구현한다.

 

1. 초기화 함수

def __init__(self, k):
        """
        :type k: int
        """

        self.head, self.tail = ListNode(None), ListNode(None)
        self.k, self.len = k, 0
        self.head.right, self.tail.left = self.tail, self.head

초기화 함수의 입력값으로 데크의 길이를 나타내는 k를 설정한다.

먼저 앞서 말한 헤드와 테일 포인터를 빈 연결리스트(ListNode(None))로 생성한다.

또한 특정 시점의 데크 길이를 표현할 값(self.len)도 0으로 초기화 한다.

 

2. 삽입 함수

def _add(self, node, new):
        n = node.right
        node.right = new
        new.left, new.right = node, n
        n.left = new

def insertFront(self, value):
        """
        :type value: int
        :rtype: bool
        """

        if self.len == self.k:
            return False

        else:
            self.len += 1
            self._add(self.head, ListNode(value))

            return True   

def insertLast(self, value):
        """
        :type value: int
        :rtype: bool
        """

        if self.len == self.k:
            return False

        else:
            self.len += 1
            self._add(self.tail.left, ListNode(value))
            return True

삽입 함수의 구성은 간단하다.

우선, 데크의 길이가 전체 길이와 같으면 더 이상 삽입할 공간이 없으므로 False를 리턴한다.

삽입할 공간이 있으면 데크의 길이를 1만큼 증가시키고, _add() 함수를 호출하여 노드를 삽입한다.

앞 쪽 삽입(insertFront())의 경우 _add() 함수의 입력값으로 self.head가 들어가고,

뒤 쪽 삽입(insertLast())의 경우 self.tail.left가 들어가는 차이가 있다.

_add() 함수를 조금 더 살펴보자면,

입력값 노드와 본래 연결된 노드(함수에서 n으로 정의한다) 사이에 새로운 노드 new를 삽입하여 방식이다.

연산 과정은 아래 그림의 번호 순서대로 진행된다.

 

image

 

3. 제거 함수

def _del(self, node):
        n = node.right.right
        node.right = n
        n.left = node

def deleteFront(self):
        """
        :rtype: bool
        """

        if self.len == 0:
            return False

        else:
            self.len -= 1
            self._del(self.head)
            return True


    def deleteLast(self):
        """
        :rtype: bool
        """

        if self.len == 0:
            return False 

        else:
            self.len -= 1
            self._del(self.tail.left.left)
            return True

제거 함수의 구성도 삽입 함수와 크게 다르지 않다.

우선, 데크의 길이가 0이면 더 이상 제거할 값이 없으므로 False를 리턴한다.

제거할 값이 있다면 데크의 길이를 1만큼 줄이고 _del() 함수를 호출한다.

del() 함수에서, 입력값인 node와 오른쪽에 이어진 노드(n으로 정의된다) 간의 연결을 끊어 내기 위해서,

node를 n 오른쪽의 노드와 연결한다.

그림으로 나타내면 아래와 같다.

 

image

 

4. 나머지 및 전체 코드

삽입과 제거 함수를 제외하면 나머지 함수는 이해하기에 무리가 없다.

전체 코드는 아래와 같다.

class MyCircularDeque(object):

    def __init__(self, k):
        """
        :type k: int
        """

        self.head, self.tail = ListNode(None), ListNode(None)
        self.k, self.len = k, 0
        self.head.right, self.tail.left = self.tail, self.head


    def _add(self, node, new):
        n = node.right
        node.right = new
        new.left, new.right = node, n
        n.left = new

    def _del(self, node):
        n = node.right.right
        node.right = n
        n.left = node

    def insertFront(self, value):
        """
        :type value: int
        :rtype: bool
        """

        if self.len == self.k:
            return False

        else:
            self.len += 1
            self._add(self.head, ListNode(value))

            return True


    def insertLast(self, value):
        """
        :type value: int
        :rtype: bool
        """

        if self.len == self.k:
            return False

        else:
            self.len += 1
            self._add(self.tail.left, ListNode(value))
            return True

    def deleteFront(self):
        """
        :rtype: bool
        """

        if self.len == 0:
            return False

        else:
            self.len -= 1
            self._del(self.head)
            return True


    def deleteLast(self):
        """
        :rtype: bool
        """

        if self.len == 0:
            return False 

        else:
            self.len -= 1
            self._del(self.tail.left.left)
            return True



    def getFront(self):
        """
        :rtype: int
        """

        return self.head.right.val if self.len else -1


    def getRear(self):
        """
        :rtype: int
        """

        return self.tail.left.val if self.len else -1


    def isEmpty(self):
        """
        :rtype: bool
        """

        return self.len == 0


    def isFull(self):
        """
        :rtype: bool
        """

        return self.len == self.k

# Your MyCircularDeque object will be instantiated and called as such:
# obj = MyCircularDeque(k)
# param_1 = obj.insertFront(value)
# param_2 = obj.insertLast(value)
# param_3 = obj.deleteFront()
# param_4 = obj.deleteLast()
# param_5 = obj.getFront()
# param_6 = obj.getRear()
# param_7 = obj.isEmpty()
# param_8 = obj.isFull()

본 포스팅은 '파이썬 알고리즘 인터뷰(책만, 박상길)을 참조함

1. 문제 - 일일온도(리트코드 739)

- 문제: 매일 화씨 온도 리스트 T를 입력받아서, 더 따뜻한 날씨를 위해서는 며칠을 더 기다려야 하는지를 출력하라

    - Input: temperatures = [73,74,75,71,69,72,76,73]
    - Output: [1,1,4,2,1,1,0,0]

- 설명: 위 온도 배열에서 첫 번째 값은 73도이고 그 다음은 74도다. 날씨가 더 따뜻해지려면 1일만 기다리면 되므로 출력값은 ‘1’이된다.

2. 풀이 - 스택 값 이용

개요

전체적인 풀이 과정은 아래와 같다

  1. (for 반복문) for 반복문으로 온도 리스트를 참조한다.
  2. (스택) 이를 통해 요소의 인덱스를 스택에 저장한다.
  3. (while문) 현재 온도(for문)가 이전 온도(스택)보다 큰지 여부를 판단한다
  4. (결과값) 현재 온도와 이전 온도의 인덱스 차이(=날짜 차이)를 리턴한다

해설

우선 아래 그림으로 연산 과정을 살펴보겠다.

image

F’는 입력값인 온도 리스트를 말한다. 

for 문을 통해 인덱스5(온도 72)가 참조 되었다고 하자.

이 때, 인덱스5보다 기온이 낮은 인덱스4(69도)와 인덱스3(71도)는 스택에서 대기를 하고 있는 상태다. 

‘for문이 가리키는 인덱스 5의 온도’가 ‘스택의 마지막 값이 가리키는 인덱스4’의 온도보다 크기 때문에

인덱스4는 스택에서 제거되고 인덱스5와 인덱스4의 차이값(날짜 차이) 1이 출력된다.

스택에서 인덱스4가 출력된 후, 스택의 맨 마지막에는 인덱스 3이 남게된다.

while문을 통하여 앞서 말한 과정과 동일하게 인덱스5와 인덱스3 간의 날짜 차이가 구해진다.

코드

def dailyTemperatures(self, temperatures):
  """
  :type temperatures: List[int]
  :rtype: List[int]
  """

  stack = []

  result = []

  for i, f in enumerate(temperatures):

      # 현재 온도 > 스택 온도 => (인덱스)거리를 정답으로 등록
      while stack and temperatures[stack[-1]] < f:
          prev = stack.pop()
          result[prev] = i - prev

      stack.append(i)
      result.append(0)

  return result

<목차>

1. 개념

2. 예제

 

 

 

1. 개념


분할정복(Divide and Conquer)이란 문제를 직접 해결하기 쉬운 단위로 잘게 나눈 후(분할) 작은 문제의 답을 구하여(정복) 저장하고,

 

작은 문제의 답을 조합하여 전체 문제를 해결하는 방법이다.

 

아래 그림과 수도코드에서 분할, 정복, 조합에 해당하는 부분을 표시해보았다.

 

분할 정복 그래프 및 수도 코드

 

 

 

 

2. 예제 -과반수 엘리먼트(리트코드 169)


1) 문제 

 

배열을 중에서 전체 엘리먼트의 절반 이상을 차지하는 값을 출력할 것을 요구한다.

 

입력 및 출력

위 예시를 보면, 리스트 내 7개의 엘리먼트 중 '정수 2'가 4개로  절반 이상을 차지하므로

 

결과 값으로 2가 출력된 것을 알 수 있다.

 

 

 

2) 풀이

 

앞서 개념에서 설명한 것처럼 분할 정복 기법은 분할, 정복, 조합 부분으로 이뤄진다.

 

전체 코드는 다음과 같으며 분할, 정복, 조합 파트로 세분화하여 살펴보겠다.

 

 

 

(1st) 분할

재귀 함수를 호출할 때, 슬라이싱을 활용하여 입력값(리스트, nums)의 범위를 조정한 한다.

 

하나의 배열을 두 개로 쪼개는데,

 

첫 번째 배열(a)은 왼쪽 절반([ : len(nums) // 2] ), 두 번째 배열은 오른쪽 절반( [len(nums) // 2 : ] )으로 슬라이싱 처리한다.

 

최종적으로 분할된 모습은 아래와 같을 것이다.

 

 

 

 

(2nd) 정복

끝까지 쪼개면 배열에 값이 하나만 존재할 것이다.

 

따라서 배열의 길이가 1이 되면 그 값(정수형)을 반환한다.

 

 

 

(3rd) 조합

 

정복으로 얻은 하위 문제 해답을 조합하여 전체 문제의 답을 구해나간다.

 

왼쪽 하위 문제에서 절반 이상을 차지하던 값(a)이 상위 문제 배열에서도 절반 이상(   > len(nums) // 2   )을 차지할 때, 

 

True(정수 1)가 반환되므로 결국 a가 리턴된다.

 

반대의 경우는 b가 리턴될 것이다. 

 

 

이처럼 하부 문제를 구하는 과정을 반복하여 전체 문제의 해답을 도출한다.

다이나믹 프로그래밍(Dynamic Programming) 알고리즘을 피보나치수열을 예로 들어 설명해보려 한다.

 

해당 문제는 리트코드(LeetCode) 85번 문제를 참조하면 된다.

 

 

< 목차 > 

1. 다이나믹 프로그래밍의 의미

2. 다이나믹프로그래밍의 특성

3. 다이나믹프로그래밍 방법론

4. 다이나믹프로그래밍 의의

 

 

 

 

1. 다이나믹 프로그래밍 의미


다이나믹 프로그래밍(Dynamic Programming)이란 하나의 문제를 여러 개의 하부 문제로 분할하고,

하부 문제를 해결하고 그 결과를 저장하여,

전체 문제 풀이에 더해가며 문제를 해결하는 알고리즘이다.

 

 

 

 

2. 다이나믹 프로그래밍의 특성


다이나믹 프로그래밍으로 해결할 수 있는 문제는 두 가지 특성을 충족해야 한다.

 

1. 부분 최적 구조(Optimal Substructure)

전체 문제의 최적 해결 방법이 부분 문제의 최적 해결 방안으로 구성됨을 의미한다.

 

2. 중복된 하위 문제(Overlapping Sub-problem)

전체 문제를 풀이하는 과정이 여러 번 나타나는 하부 문제 해답으로 구성되는 구조를 말한다.

 

 

피보나치 수열 도식화

 

피보나치수열 예시를 통해 앞서 말한 두 가지 특성을 알아보자.

 

 

피보나치수열은 이전 두 개의 값을 더하여 새로운 값을 구하는 방식이며,

 

수식으로 표현하면 다음과 같다.

 

F(n) = F(n-1) + F(n-2)

 

F(0) = 0이고, F(1) =1일 때, F(2)는 F(1)과 F(2)를 더한 값, 즉 2가 된다.

 

 

피보나치수열은 하부 문제를 해결함으로써 전체 문제에 대한 답을 구할 수 있다.(최적 부분 구조)

 

위에서 말한 수식으로 F(0)부터 계산을 해나가면 그림 상 맨 꼭대기에 있는 F(4)를 구할 수 있다.

 

위 그림에서 F(2)의 경우 꼭대기 F(4)를 기준으로 왼쪽에도 존재하고 오른쪽에도 존재한다.

 

즉 F(4)를 구하기 위해 F(2)가 중복해서 계산이 되는 구조다.(중복된 하위 문제)

 

 

 

 

3. 다이나믹 프로그래밍 방법론


동적 프로그래밍 알고리즘을 이용하여 문제를 해결하는 방법에는 크게 두 가지가 있다. 

 

 

1. 상향식(Bottom-Up)

 

타뷸레이션(tabulation) 이라고도 한다.

 

하위 문제를 먼저 계산한 후, 그 값을 메모리에 저장해둔다.

 

저장된 하위 문제 값을 토대로 전체 문제 값을 계산해나간다.

 

일반적으로 반복적(Iterative) 방법을 이용해서 구현한다.

 

 

2. 하향식(Top-Down)

 

메모이제이션(Memoization)이라고도 부른다.

 

일반적으로 재귀적(Recursive) 방법으로 계산하며,

 

이전에 계산된 하부 문제 값이 존재하는지 참조하며 재귀 호출을 진행하는 방식이다.

 

 

아래는 두 가지 방법으로 피보나치수열을 구현한 코드다.

 

(손으로 지저분하게 작성한 점 양해 바란다)

 

 

(피보나치수열) 타뷸레이션 및 메모이제이션 구현

 

 

 

 

4. 다이나믹 프로그래밍 의의


타뷸레이션과 메모이제이션을 활용하면 미리 저장된 하부 문제의 답을 참조할 수 있기 때문에 연산 과정이 빨라진다.

 

 

피보나치 수열 (좌: 브루트 포스 / 우: 다이나믹 프로그래밍)

 

왼쪽은 브루트 포스 방식으로 구현한 피보나치수열이고, 오른쪽은 다이나믹 프로그래밍(타뷸레이션, 메모이제이션)으로 구현한 그래프다.

 

왼쪽 그림에서는 모든 경우의 수를 다 적용하여 해답(f(4))을 구한 방식을 그래프로 나타냈다.

 

반면 오른쪽 그래프에서  꼭대기를 기준으로 오른쪽 부분을 보면 일부가 잘려 있음을 알 수 있다.

 

f(2)의 경우 꼭대기 기준 왼쪽 부분에서 하부 문제로 해답을 이미 구했기 때문에,

 

오른쪽에서는 f(2)를 다시 쪼개어 정답을 구하지 않고 왼쪽 f(2) 값을 참조한 것이다.

 

 

이처럼 다이나믹 프로그래밍 알고리즘으로 '최적 부분 구조' 및 '중복 하부 문제'의 특성을 지닌 문제를 효율적으로 풀 수 있다. 

 

 

 

 

 

* 본 포스팅은 도서 '파이썬 알고리즘 인터뷰(책만 / 박상길)'을 참조하여 작성했습니다.

알고리즘 '비트 조작' 파트의 코딩 문제를 풀기 위해

 

꼭 이해해야할 개념인 '부울 연산자', '비트 연산자', '2의 보수'에 관해 알아보고자 한다.

 

 

<목차>

1. 부울 연산자(Boolean Operator)

2. 비트 연산자 (Bitwise Operator)

3. 보수(Complement)

4. 2의 보수 구하기

5. 2의 보수를 이용한 수학 연산

 

 

1.  부울 연산자(Boolean Operator)


 

1.1 개념 

부울(Bool)참(True), 거짓(False)를 나타내는 자료형으로, 

 

부울 연산자는 특정 수들의 논리 연산에 사용되는 기호이다.

 

부울 연산자

 

NOT, AND, OR의 개념은 친숙하지만,

 

XOR은 생소할 수 있겠다.

 

'Exclusive OR'이라고 읽으며, 

 

값이 서로 다를 때(T-F or F-T)만 참(True)이되는 연산자다.

 

 

1.2 예시코드

부울 연산자 예시 코드

 

 

 

2. 비트 연산자(Bitwise Operator)


 

2.1 기호

부울 연산자를 비트 연산자로 표현할 수 있다.

 

비트 연산자

 

2.2 예시 코드

비트 연산자 코드 예시

위 사례에서 주목할 부분은 비트 연산자 NOT(~, 틸트)이다.

 

틸트를 부울 변수 False(=0)에 적용할 때  -1이 출력된다.

 

연산자 NOT은 2의 보수에서 1을 뺀 값과 같기 때문이다.

 

그러면 2의 보수는 무엇일까?

 

 

 

 

3. 보수(Complement) 


3.1 개념

2의 보수에 앞서, 보수의 기본 개념에 대해서 살펴보자.

 

보수는 컴퓨터가 음수를 계산하기 위해 취하는 방법의 일종이다.

 

컴퓨터는 가산기를 이용하여 덧셈, 뺄셈, 곱셈, 나눗셈을 계산한다.

 

즉, 덧셈만 이용하여 뺄셈, 곱셈, 나눗셈을 계산해야하는 것이다.

 

곱셈은 덧셈을, 나눗셈은 뺄셈을 반복하면 된다.

 

그럼 덧셈을 이용하여 뺄셈은 어떻게 계산할까?

 

이때 등장하는 개념이 '보수'다.

 

 

3.2 예시 

보수를 활용한 뺄셈 연산 사례

컴퓨터가 '6 - 2 = 4' 를 계산해야 한다고 하자.

 

하지만 컴퓨터는 '-2'를 직접 계산할 수 없기 때문에, 

 

덧셈을 이용해서 뺄셈(-2)을 연산해야 한다.

 

'-2'의 (10의)보수는 '-2'에서 '+10'만큼 이동한 '+8'이다.

 

'6'과 '-4'의 보수인 '+8'을 더한 값(6 + 8)은 '14'다.

 

'14'에서 십의 자리수 '1'을 제외하고 '4'만 출력한다.

 

그럼 본래 (6-2 의)답 '4'와 동일한 결과를 얻을 수 있다.

 

 

 

 

4. 2의 보수 구하기


 

4.2.1 사례 

-14의 (2의)보수 구하기

십진수 -14의 보수를 구하려고 한다.

 

십진수를 이진수로 바꾸면 -1110이 된다.

 

-1000을 반전시키면 1의 보수(+0001)을 구할 수 있다.

(편의상 반전이라 칭했으나, 실제로는 마스크 값(1111)와 XOR 연산한 값이다.)

1의 보수에 1을 더하면 2의 보수를 구할 수 있다. 

 

 

 

 

5. 2의 보수를 이용한 연산

 

5.2.1 사례


보수를 이용하여 '14 - 10 = 4' 계산하기

 

컴퓨터가 '14 - 10 = 4'을 계산한다고 하자.

 

거듭 말했듯이 컴퓨터는 음수(-10)을 직접 계산할 수 없기에, 보수를 활용한다.

 

우선 '14 - 10 = 4'을 이진수로 나타내면, '1110 - 1010 = 0100'이 된다. 

 

'-1010'의 2의 보수를 나타내기 위해, 

 

우선 반전을 통해 1의 보수(+0101)을 구한 다음,

 

1을 더해주면 2의 보수(+0110)를 구할 수 있다. 

 

-10의 2의 보수를 구한 후, 본래 식을 계산하면 '10100'이 나온다.

 

맨 앞에 1(자릿수 올림을 통해 새로 생겨난 값으로 '캐리'라고 한다)을 버리면 '0100'으로,

 

2의 보수를 구하기 전의 결과 값과 동일하다. 

 

 

 

* 본 포스팅은 도서 '파이썬 알고리즘 인터뷰' 및 유튜브 '대멀쌤의 자료를 참고했습니다.(https://www.youtube.com/c/%EB%8C%80%EB%A9%80%EC%8C%A4)

 

 

 

클래스를 정의할 때,

 

항상 첫 번째 함수는 초기화 함수로 시작하곤 했다.

 

사실 __init__ 메서드를 

 

왜 쓰는지 모르고 그냥 써왔다. 

 

초기화 함수를 지정하지 않고 class를 정의 해본다면

 

초기화 함수의 역할을 가장 간단히 이해할 수 있다.

 

1. 초기화 함수를 사용하지 않고 class를 정의

 

숫자 1~5까지 리스트 샘플을 만드는 클래스 내에,

 

초기화 함수 없이 숫자를 추가하고, 리스트를 출력하는 함수를 생성했다.

 

 

클래스 객체를 생성하고 숫자 6을 추가한 후, 해당 리스트를 출력하면

 

[1, 2, 3, 4, 5, 6]이 출력된다.

 

 

다시 클래스 객체를 생성하고 이번에 숫자 7을 추가한 후, 리스트를 출력하면

 

[1, 2, 3, 4, 5, 7]이 아니라 [1, 2, 3, 4, 5, 6, 7]이 출력된다.

 

클래스를 새로 생성하더라도, 초기값 리스트(1~5까지 숫자)가 아니라,

 

직전에 만들어진 객체를 그대로 받아서 쓴다.

 

 

 

2. __init__ 메서드를 사용하여 class를 정의

 

반면에 __init__ 메서드를 생성한 후,

 

클래스 객체를 새로 생성하면,

 

새로 생성된 객체는 초기화 되어 있는 사실을 알 수 있다. 

 

 

즉, 이전에 생성한 클래스 객체에 숫자 6을 추가했더라도,

 

클래스 객체를 새로 생성하면 숫자 6은 사라지고, 초기값(1~5)만 남아 있는 것이다.

 

 

 

 

 

 

 

 

트리 순회는 그래프 순회의 일종으로,

 

트리 자료구조에서 각 노드를 한 번 방문하는 과정을 말한다.

 

DFS 또는 BFS로 탐색 하는데,

 

이진 트리에서 DFS는 노드의 방문 순서에 따라 크게 3가지 방식으로 구분된다.

 

1. 전위(Pre-Order) 순회(NLR)

현재 노드(N) -> 왼쪽 서브트리(L) -> 오른쪽 서브트리(R) 순서대로 탐색하는 방식이다

 

재귀 구조로 구현한 코드와 그래프는 아래와 같다

 

 

Pre-Order 순회

 

빨간색 선으로 이어진 순서대로(A - B - D - F - H - I -C -G -J ) 값이 출력된다.

 

 

2. 중위(In-Order) 순회

 왼쪽 서브트리(L) -> 현재 노드(N) -> 오른쪽 서브트리(R) 순서대로 탐색하는 방식이다.

In-Order 순회

출력 순서는 D - B - G -E -H - A -C - I -F 이다

 

 

3. 후위(Post-Order) 순회

왼쪽 서브트리(L) -> 오른쪽 서브트리(R) -> 현재 노드(N) 순서대로 탐색하는 방식이다.

 

Post-Order 순회

출력 순서는 D - G - H - E - B - I - F -C -A 순서이다.

 

 

 

트리 순회의 직관적인 이해가 필요한 사람은 아래 영상을 참조해도 좋겠다.

https://www.youtube.com/watch?v=bOZhvOc5xlQ&t=223s

 

+ Recent posts