Radix Sort, Suffix Sort
Strings in Java
문자열은 Character (문자) 의 나열이다. C 에서 하나의 캐릭터는 8-bit 인데, 자바의 경우에는 16-bit unsigned integer 로 표시한다.
스트링의 길이를 얻기 위해 length, 인덱싱 하기 위해 charAt, 서브스트링을 얻기 위해 substring 의 메소드를 지원한다.
public final class String implements Comparable<String> {
private char values;
private int offset; // index of first char in array
private int length;
private int hash; // cache of hashCode()
...
public char charAt(int i) {
return value[i + offset];
}
...
}
자바에서 문자열은 immutable 이다. 더 정확히는 immutable char [] array 라 보면 된다. 길이 정보를 가지고 있고 배열이기 때문에 length, charAt, substring 등의 연산은 O(1) 임을 보장한다.
concat 의 경우에는 새로운 문자열을 만들기 때문에 O(N) 이다. 메모리는 길이 N 의 문자열에 대해 40 + 2N 을 필요로 한다. 메모리를 아껴야 한다면 byte, char 을 이용할 수 있겠지만 여러 편리한 스트링의 메소드를 사용하지 못한다.
StringBuilder, StringBuffer
StringBuilder 는 mutable 이다. char [] 배열을 resizing 하기 때문에
substirng의 경우O(N)이며 (새 스트링을 만든다)concat은O(1*)이다. (*는 amortized)
length, charAt 은 마찬가지로 O(1) 이다. 참고로 StringBuffer 는 StringBuilder 와 비슷하지만 thread safe 하고, 느리다.
그러면 reverse 를 구현 할 때 String 과 StringBuilder 중 어떤 것이 더 나을까?
// 1. use String
public static String reverse(String s) {
String rev = "";
for (int i = s.length() - 1; i >= 0; i--)
rev += s.charAt(i);
return rev;
}
// 2. use StringBuilder
public static String reverse(String s) {
StringBuilder rev = new StringBuilder();
for (int i = s.length() - 1; i >= 0; i--)
rev.append(s.charAt(i));
return rev.toString();
}
String 을 이용한 버전은 O(n^2) 이고, StringBuilder 를 이용한 버전은 O(n) 이다. 이는 += 와 append 의 차이 때문이다.
suffixes 문제도 생각해 보자.
// input string
a a c a a g t t a c a a g c
// output
c // suffixes 14
g c // suffixes 13
a g c // suffixes 12
...
...
a a c a a g t t a c a a g c // suffixes 0
String 과 StringBuilder 의 구현을 생각해 보면,
// 1. use String
public static String[] suffixes(String s) {
int N = s.length();
String[] suffixes = new String[N];
for (int i = 0; i < N; i ++)
suffixes[i] = s.substring(i, N);
return suffixes;
}
// 2. use StringBuilder
public static String[] suffixes(String s) {
int N = s.length;
stringBuilder sb = new StringBuilder(s);
String suffixes = new String
for (int i = 0; i < N; i++)
suffixes[i] = s.substring(i, N);
return suffixes;
}
당연히 substring 은 String 이 메모리 사용량이 훨씬 더 적을꺼라 생각했는데 Java7 Update6 부터 좀 달라졌다고 한다.
Java 7 Update 6 부터는 이전처럼 String 의 char [] 가 공유되지 않는단다. 따라서 String.substring 은 더이상 constance space, time 이 아니라 linear space, time 의 비용이 든다. 자세한 내용은 Changes to String Java 1.7.0-06로
따라서 알고리즘 1 은 linear time, space 2 는 quadratic time, space 의 알고리즘이다.
Longest common prefix
public static int lcp(String s, String t) {
int n = Math.min(s.length(), t.length());
for (int i = 0; i < n; i++)
if (s.charAt(i) != t.charAt(i))
return i;
return n;
}
러닝타임은 s, t 중 더 긴 문자열의 길이에 비례한다. 일반적으로는 sublinear time 이다. 따라서 compareTo 메소드를 sublinear time 으로 구현할 수 있다.
Radix
알파벳을 다양한 형태로 표현할 수 있는데, binary 의 경우엔 01 이 될 것이다. 이때의 radix 는 2 다. DNS 는 ACTG 로 표현할 수 있으므로 R = 4 다.
Key-Indexed Counting
정렬 알고리즘의 성능을 정리해 보면,
(1) Insertion Sort
- guarantee:
O(N^2 / 2) - random:
O(N^2 / 4) - extra space:
1 - stable:
yes
(2) Merge Sort
- guarantee:
O(N log N) - random:
O(N log N) - extra space:
N - stable:
yes
(3) Quick Sort
- guarantee:
O(1.39 N log N) - random:
O(1.39 N log N) - extra space:
c log N - stable:
no
(4) Heap Sort
- guarantee:
O(2 N log N) - random:
O(2 N log N) - extra space:
1 log N - stable:
no
이런 comparison based 알고리즘은 lower bound 가 N log N 이다. 따라서 key compare 를 하지 않는다면 더 나은 성능을 낼 수 있다.
key-indexed counting 에서는 key 가 0 부터 R - 1 사이의 정수라 가정한다. 따라서 키를 배열의 인덱스로 사용할 수 있다.
따라서 다음처럼 활용할 수 있다.
- Sort String by first letter
- Sort class roster by section
- Sort phone number by area code
- Subroutine in a sorting algorithm
알고리즘을 보자.
Goal: Sort an array
a[]ofNintegers between0andR - 1
(1) Count frequencies of each letter using key as index
(2) Compute frequecy cumulates which specify destinations
(3) Access cumulates using key as index to move items
(4) Copy back into original array
int N = a.length();
int[] count = new int[R + 1];
// step (1)
for (int i = 0; i < N; i++)
count[a[i] + 1]++;
// step (2)
for (int r = 0; r < R; r++)
count[r + 1] += count[r];
// step (3)
for (int i = 0; i < N; i++)
aux[count[a[i]]++] = a[i];
// step (4)
for (int i = 0; i < N; i++)
a[i] = aux[i];
~11N + 4Rarray accessN + Rextra space
key-indexed counting 은 linear time, stable sorting 이다.
Stable
알고리즘이 stable 하다는 건 무슨 뜻일까?
A stable sort is one which preserves the original order of the input set while The unstable algorithm exhibits undefined behaviour when two elements are equal, it is perfectly possible that the order is sometimes preserved.
(http://programmers.stackexchange.com/)
LSD Radix Sort
least-significant-digit-first string(radix) sort
아이디어는 간단하다. 우측부터 좌측으로 한 문자씩 key-indexed couting 을 하면 된다.
(www.programering.com)
Which of the following is the most efficient algorithm to sort 1 million 32-bit integers?
답은 radix sort
Correctness
LSD sorts fixe-length strins in ascending order
가설에 의해 i 번째 pass 후에는 뒤 부터 i 개의 문자들이 정렬되어 있다. 이 때 비교하려는 i+1 번째의 두 문자가 다르다면, key-indexed sort 가 두 개의 문자열을 정렬한다. 이 때 key-indexed sort 는 stable 하므로 이전 까지의 정렬했던 순서를 보존한다.
Implementation
// W: fixed-length of strings
public static void LSDsort(String[] a, int W) {
int N = a.length;
int R = 256;
String[] aux = new String[N];
// key indexed counting for each digit from right to left
for (int d = W - 1; d >= 0; d--) {
int[] count = new int[R + 1];
// count frequencies
for (int i = 0; i < N; i++)
count[a[i].charAt(d) + 1]++;
for (int r = 0; r < R; r++)
count[r + 1] += count[r];
for (int i = 0; i < N; i++)
aux[count[a[i].charAt(d)]++] = a[i];
for (int i = 0; i < N; i++)
a[i] = aux[i];
}
}
LSD sort 퍼포먼스는 2WN, 랜덤하게 2WN, 공간은 N + R, stable 하다. 참고로, 4byte Int 에 대해 1Byte 씩 LSD sort 를 적용하면 Array.sort 보다 2~3배 더 빠르다고 한다. 코드는 여기로
MSD Radix Sort
most significant-digit-first string sort
- Partition array into
Rpieces according to first character - Recursively sort all strings that start with each character
좌측 문자열 부터 시작하고, 현재 문자가 같은 문자열들 끼리 모아, 나머지 부분을 sub-array 취급해서 재귀적으로 정렬한다.
(www.programering.com)
LSD 는 다루지 못하는 variable-length string 을 정렬할 수 있다.
Implementation
참고로 자바에서는