컴퓨터는 내부적으로 모든 자료를 이진수(비트)로 처리한다. 이런 컴퓨터의 연산방식을 이용한, 정수의 이진수 표현을 활용하여 문제를 해결하는 기법을 말한다.
비트는 이진수(0
과1
로 구성된 수)를 나타내는 말로 컴퓨터에서 사용하는 데이터의 최소 단위이다.
비트는 1
과0
의 값을 가질 수 있고 true(1)
또는 false(0)
라는 상태를 나타낼수도 있다.
우리가 사용하는 10
진수는 0
과1
로 구성된 비트(이진수)로 표현이 가능하다.
O(1)
의 시간복잡도를 갖는다.비트연산의 종류
and(&)
, or(|)
, xor(^)
, not(~)
두 수 A
,B
를 비트 연산하는 경우 가장 뒤의 자리부터 하나씩 연산해주면 된다.
[and 연산 계산과정]
A = 27, B = 83 인 경우, A와 B를 이진수로 표현하면 아래와 같다.
A = 11011, B = 1010011
이 경우 And 연산을 하면 아래와 같다.
0 0 1 1 0 1 1
& 1 0 1 0 0 1 1
-----------------
0 0 1 0 0 1 1
답 : A&B = 19
and 연산의 경우 각 자리수를 비교하여 두 자리가 모두 1
일 경우에만 1
하나만 1
이거나 둘다 모두 0
일경우 0
으로 계산한다.
A와 B는 자리수가 다르므로 이를 맞춰주기 위해 A의 앞에 0
을 B의 개수에 맞춰 추가해 준다. (자릿수가 적은쪽 앞에 0을 추가)
[or 연산과정]
A = 27, B = 83 인 경우, A와 B를 이진수로 표현하면 아래와 같다.
A = 11011, B = 1010011
이 경우 And 연산을 하면 아래와 같다.
0 0 1 1 0 1 1
| 1 0 1 0 0 1 1
-----------------
1 0 1 1 0 1 1
답 : A|B = 91
1
일 경우 1
로 두 숫자 모두 0
일경우에만 0
으로 계산한다.[xor 연산과정]
A = 27, B = 83 인 경우, A와 B를 이진수로 표현하면 아래와 같다.
A = 11011, B = 1010011
이 경우 And 연산을 하면 아래와 같다.
0 0 1 1 0 1 1
^ 1 0 1 0 0 1 1
-----------------
1 0 0 1 0 0 0
답 : A^B = 72
0
과 1
일경우 1
나머지의 경우 0
으로 계산한다.[not 연산과정]
A = 83인 경우, A를 이진수로 표현하면 아래와 같다.
A = 1010011
이 경우 not 연산을 하면 아래와 같다.
8비트 ~A : 0101100
32비트 ~A : 11111111111111111111111110101100
답 : ~A = 72
0
-> 1
그리고 1
-> 0
으로 바꾸는 방식으로 계산한다.A << B
위 식은 A를 B만큼 왼쪽으로 밀어준다 라는 의미를 갖는 식이다.
밀어준만큼 동시에 0을 추가해준다.
shift left연산의 예를 살펴보자.
1 << 0 -> 1
1 << 1 -> 2 (10)
1 << 2 -> 4 (100)
1 << 3 -> 8 (1000)
1 << 4 -> 16 (10000)
3 << 3 -> 24 (11000)
5 << 10 -> 5120 (1010000000000)
A << B
식은 A * 2의 B제곱
과 같다.A >> B
위 식은 A를 B만큼 오른쪽으로 밀어준다 라는 의미를 갖는 식이다.
밀어준만큼 동시에 0을 추가해준다.
shift right연산의 예를 살펴보자.
1 >> 0 -> 1
1 >> 1 -> 0 (0)
10 >> 1 -> 5 (101)
10 >> 2 -> 2 (10)
10 >> 3 -> 1 (1)
30 >> 1 -> 15 (1111)
1024 >> 10 -> 1 (1)
A >> B
식은 A / 2의 B제곱
과 같다.비트마스크를 이용해 정수로 집합을 표현할 수 있다.
비트가 1(true)
이면 해당 원소가 존재한다는 의미이고 비트가 0(false)
이면 해당 원소가 존재하지 않는다는 의미이다.
N
비트 정수 변수라면 N
개의 원소를 갖는 집합의 부분집합들을 모두 표현할 수 있다.
원래는 N
개의 원소를 갖는 boolean
배열을 선언해야 했지만 비트마스크를 이용하면 하나의 정수로 표현이 가능하기 때문에 사용하는 메모리의 크기가 굉장히 줄어든다.
[비트마스크를 이요한 정수 집합 표현]
예를 들어 [1, 3, 4, 5, 9]
라는 집합이 있다고 생각해보면, 아래 그림처럼 비트로 표현이 가능하다.
10
까지의 원소중 [1, 3, 4, 5, 9]
에 해당하는 원소만 집합에 존재하므로 해당 번호에 해당하는 원소값만 1
로 표현해주었다.
따라서 [1, 3, 4, 5, 9] = 570
의 정수로 표현이 가능하다.
즉, 원소의 개수가 N
인 집합이 있다고 가정하면, 각 원소의 번호는 0
부터 N-1
까지 부여가 가능하다.
또한 각 번호에 해당하는 원소가 존재하면 비트가 1
, 존재하지 않으면 비트는 0
으로 표현이 가능하다.
K
번째 원소가 포함되었는지 여부를 확인하고 싶다면 and
연산을 이용해서 K
번째 비트가 1
인지 여부만 확인하면 된다.집합A = [1, 3, 4, 5, 9] = 570
2번째 원소가 포함되었는지 여부 확인 => A & (1 << 2)
2번째 비트의 위치에만 1이 있고 나머지 비트의 값은 모두 0이다.
이 값에 집합A와 and(&)연산의 계산값이 0이 아니라면 2번째 원소의 값은 존재한다는 것을 알 수 있다.
1 0 0 0 1 1 1 0 1 0
0 0 0 0 0 0 0 1 0 0
-------------------
0 0 0 0 0 0 0 0 0 0
3번째 원소가 포함되었는지 여부 확인 => A & (1 << 3)
3번째 비트의 위치에만 1이 있고 나머지 비트의 값은 모두 0이다.
이 값에 집합A와 and(&)연산의 계산값이 0이 아니라면 3번째 원소의 값은 존재한다는 것을 알 수 있다.
1 0 0 0 1 1 1 0 1 0
0 0 0 0 0 0 1 0 0 0
-------------------
0 0 0 0 0 0 1 0 0 0
1
이므로 해당 원소는 존재한다는 사실을 알 수 있다.k
번째 원소 포함여부 검사 공식은 A & (1 << k)
int x = 32;
System.out.println("4번째 원소 검사: " + Integer.toBinaryString(x & (1 << 3)));
//실행결과
4번째 원소 검사: 1000
A
집합에 특정 원소를 추가하는 방법이다. 원소에 해당하는 bit
만 1
로 바꾸어주면 된다. 때문에 해당 bit
를 항상 1
로 만드는 연산이 필요하므로 or
연산을 이용한다.
단, 이미 해당원소가 존재하는경우 아무런 변화가 없다.
집합A = [1, 3, 4, 5, 9] = 570
2번째 원소 추가 => A | (1 << 2)
2번째 비트의 위치에만 1이 있고 나머지 비트의 값은 모두 0이다.
이 값에 집합A와 or(|)연산의 계산값이 무조건 1이 나오므로 2번째 원소의 값이 추가됨을 알 수 있다.
1 0 0 0 1 1 1 0 1 0
0 0 0 0 0 0 0 1 0 0
-------------------
1 0 0 0 1 1 1 1 1 0
3번째 원소가 포함되었는지 여부 확인 => A | (1 << 3)
3번째 비트의 위치에만 1이 있고 나머지 비트의 값은 모두 0이다.
이 값에 집합A와 or(|)연산의 계산값 무조건 1이 나오므로 3번째 원소의 값이 추가됨을 알 수 있다.
1 0 0 0 1 1 1 0 1 0
0 0 0 0 0 0 1 0 0 0
-------------------
1 0 0 0 1 1 1 0 1 0
1
이므로 해당 원소가 추가되었다는 사실을 알 수 있다.int x = 32;
System.out.println("4번째 원소 추가: " + Integer.toBinaryString(x |= (1 << 3)));
//실행결과
4번째 원소 추가: 101000
A
집합에 특정 원소를 삭제하는 방법이다. 원소에 해당하는 bit
만 0
로 바꾸어주면 된다. 때문에 해당 bit
를 항상 0
로 만드는 연산이 필요하므로 and
연산을 이용한다.
단, 이미 해당원소가 존재하는 경우에만 가능하다. 만약, 해당원소가 존재하지 않으면 다른 원소의 포함 여부까지 변경될 수 있기 때문이다.
집합A = [1, 3, 4, 5, 9] = 570
2번째 원소 삭제 => A & ~(1 << 2)
(1<<2) 연산을 수행하면 2번째 비트의 위치에만 0이 있고 나머지 비트의 값은 모두 1이다.
여기에 not 연산을 수행하면 2번째 비트의 위치에만 1이 있고 나머지 비트의 값은 모두 0이다.
이 값에 집합A와 and(&)연산을 수행하면 2번째 원소의 값만 0으로 변한다.
1 0 0 0 1 1 1 0 1 0
1 1 1 1 1 1 1 0 1 1
-------------------
1 0 0 0 1 1 1 0 1 0
3번째 원소가 포함되었는지 여부 확인 => A | (1 << 3)
여기에 not 연산을 수행하면 3번째 비트의 위치에만 1이 있고 나머지 비트의 값은 모두 0이다.
이 값에 집합A와 and(&)연산을 수행하면 3번째 원소의 값만 0으로 변한다.
1 0 0 0 1 1 1 0 1 0
1 1 1 1 1 1 0 1 1 1
-------------------
1 0 0 0 1 1 0 0 1 0
1 << k
는 k
번째 비트만 켜진 상태이므로 여기에 not
연산을 하면 k
번째 비트만 꺼진 상태가 된다. 이 값을 활용해 and
연산을 수행하면 k
번째 비트만 0
이되고 나머지 bit
는 변하지 않는다.int x = 32;
System.out.println("4번째 원소 삭제: " + Integer.toBinaryString(x &= ~(1 << 3)));
//실행결과
4번째 원소 삭제: 100000
A
집합에 특정 원소가 존재하면 삭제하고, 존재하지 않는다면 추가하는 방법이다.
xor
연산을 이용한다.
집합A = [1, 3, 4, 5, 9] = 570
2번째 원소 토글 => A ^ (1 << 2)
(1<<2) 연산을 수행하면 2번째 비트의 위치에만 1이 있고 나머지 비트의 값은 모두 0이다.
이 값에 집합A와 xor(^)연산을 수행하면 2번째 원소의 값이 반대값으로 바뀐다. (0 -> 1 , 1 -> 0)
1 0 0 0 1 1 1 0 1 0
0 0 0 0 0 0 0 1 0 0
-------------------
1 0 0 0 1 1 1 1 1 0
3번째 원소가 토글 => A ^ (1 << 3)
(1<<3) 연산을 수행하면 3번째 비트의 위치에만 1이 있고 나머지 비트의 값은 모두 0이다.
이 값에 집합A와 xor(^)연산을 수행하면 3번째 원소의 값이 반대값으로 바뀐다. (0 -> 1 , 1 -> 0)
1 0 0 0 1 1 1 0 1 0
0 0 0 0 0 0 1 0 0 0
-------------------
1 0 0 0 1 1 0 0 1 0
int x = 32;
System.out.println("4번째 원소 토글: " + Integer.toBinaryString(x ^ (1 << 3)));
// 실행결과
4번째 원소 토글: 101000
전체집합 : (1 << N) - 1
N
의 개수만큼 모든 비트값이 1
이다.
인덱스로 바꾸어 계산하기 때문에 -1
을 해준다.
공집합 : 0
공집합은 모든 비트가 0
이다.
int fullSetA = (1 << 5) - 1;
System.out.println("꽉찬 집합: " + Integer.toBinaryString(fullSetA));
int fullSetALong = -1;
System.out.println("32비트 꽉찬 집합: " + Integer.toBinaryString(fullSetALong));
// 실행결과
8비트 꽉찬 집합: 11111
32비트 꽉찬 집합: 11111111111111111111111111111111
A | B
A & B
A & (~B)
A ^ B
int a = 38;
int b = 33;
System.out.println("a: " + Integer.toBinaryString(a));
System.out.println("b: " + Integer.toBinaryString(b));
System.out.println("a와 b의 합집합: " + Integer.toBinaryString(a | b));
System.out.println("a와 b의 교집합: " + Integer.toBinaryString(a & b));
System.out.println("a와 b의 차집합: " + Integer.toBinaryString(a & -b));
System.out.println("a와 b의 둘중에 하나만 포함집합: " + Integer.toBinaryString(a ^ b));
// 실행결과
a: 100110
b: 100001
a와 b의 합집합: 100111
a와 b의 교집합: 100000
a와 b의 차집합: 110
a와 b의 둘중에 하나만 포함집합: 111
집합 내 존재하는 원소의 개수 : Integer.bitCount(A)
존재하는 원소 중 가장 작은 원소찾기 : int min = A & (-A)
~A + 1
: 컴퓨터가 표현하는 A
의 2
의 보수 (-A
) A
가 있다고하자.bit
를 k
라고 하면, 0~k-1
의 bit
는 모두 0
이다.~A
에서는 k
번째 bit
는 0
, 0~k-1
의 bit
는 모두 1
이다.~A + 1
을 하게 되면 k
번째 bit
는 1
, 0~k-1
의 bit
는 모두 0
이 된다.k
이후의 비트는 아무 변화가 없다.-A
와 A
를 AND
연산을 시키면 k
번째 bit
만 켜진 상태로 남게 된다.int first = A & (-A);
A : 1010,
~A+1 (-A) : 0110,
A&(-A) : 0010
A &= (A-1)
bit
를 지우고 싶다면 A-1
과 AND
시키면 된다. A
에서 1
을 빼주게 되면 가장 오른쪽에 있던 bit
는 0
이 되고 그보다 오른쪽에 있는 모든 bit
들이 1
이 되기 때문이다. ex) A &= (A-1);
A : 1010,
A-1 : 1001,
A&(A-1) : 1000