다음은 문자열이 단순 할 때 (즉, 반복되는 문자가 포함되지 않은 경우) 먼저 수행 한 작업을 표시 한 다음 전체 알고리즘으로 확장하여 Ukkonen 알고리즘을 설명하려는 시도입니다.
먼저, 몇 가지 예비 진술.
우리가 만들고있는 것은 기본적으로 검색 시도와 같습니다. 따라서 루트 노드, 그로부터 나가는 가장자리, 새로운 노드로 이어지는 가장자리, 그리고 그 밖의 다른 가장자리가 있습니다.
그러나 검색 트리와 달리 가장자리 레이블은 단일 문자가 아닙니다. 대신, 각 모서리는 정수 쌍을 사용하여 레이블이 지정됩니다
[from,to]. 이것들은 텍스트에 대한 포인터입니다. 이러한 의미에서 각 모서리는 임의 길이의 문자열 레이블을 전달하지만 O (1) 공간 (두 개의 포인터) 만 사용합니다.
기초 원리
먼저 간단한 문자열, 반복되는 문자가없는 문자열의 접미사 트리를 만드는 방법을 먼저 보여 드리고자합니다.
abc
알고리즘 은 왼쪽에서 오른쪽으로 단계적으로 작동합니다 . 문자열의 모든 문자마다 한 단계 가 있습니다 . 각 단계에는 둘 이상의 개별 작업이 포함될 수 있지만 총 작업 수는 O (n)임을 알 수 있습니다 (마지막 최종 관찰 참조).
따라서 왼쪽 부터 시작하여 먼저 a루트 노드 (왼쪽)에서 잎까지 [0,#]의 가장자리를 만들고이 레이블을로 지정 하여 단일 문자 만 삽입합니다.
즉, 가장자리는 위치 0에서 시작하여 끝나는 하위 문자열을 나타냅니다. 에서 현재 끝 . 기호 #를 사용하여 현재 끝 을 의미 하며 위치 1에 있습니다 (바로 뒤 a).
초기 트리는 다음과 같습니다.

그리고 이것이 의미하는 바는 다음과 같습니다.

이제 위치 2로 진행합니다 (바로 후 b). 각 단계의 목표는 모든 접미사를 현재 위치까지
삽입 하는 것 입니다. 우리는 이것을
- 기존
a에지 확장ab
- 하나의 새로운 가장자리를 삽입
b
우리의 표현에서 이것은

그리고 그것이 의미하는 것은 :

우리는 두 가지를 관찰합니다 .
- 위한 에지 표현
ab이다 동일한 는 초기 트리 예전 같이 [0,#]. 현재 위치 #를 1에서 2로 업데이트했기 때문에 그 의미가 자동으로 변경되었습니다 .
- 각 모서리는 표시되는 문자 수에 관계없이 텍스트에 두 개의 포인터로만 구성되므로 O (1) 공간을 사용합니다.
다음으로 위치를 다시 늘리고 c기존의 모든 모서리에 a 를 추가 하고 새 접미사에 대해 새 모서리를 하나 삽입 하여 트리를 업데이트합니다 c.
우리의 표현에서 이것은

그리고 그것이 의미하는 것은 :

우리는 관찰한다 :
- 트리는
각 단계 후 현재 위치까지 올바른 접미사 트리입니다.
- 텍스트에 문자 수만큼 많은 단계가 있습니다.
- 각 단계의 작업량은 O (1)입니다. 모든 기존 모서리는 증분
#에 의해 자동으로 업데이트 되므로 최종 문자에 대한 새 모서리 하나를 삽입하는 데 O (1) 시간이 걸릴 수 있습니다. 따라서 길이가 n 인 문자열의 경우 O (n) 시간 만 필요합니다.
첫 번째 확장 : 간단한 반복
물론 이것은 문자열에 반복이 없기 때문에 아주 잘 작동합니다. 보다 현실적인 문자열을 살펴 보겠습니다.
abcabxabcd
abc이전 예제에서 와 같이 시작한 다음 ab을 반복 x한 다음 abc을 반복합니다 d.
1 단계 ~ 3 단계 : 처음 3 단계 후 이전 예의 트리가 있습니다.

4 단계 :# 위치 4 로 이동 합니다. 이렇게하면 기존의 모든 가장자리가 암시 적으로 다음과 같이 업데이트됩니다.

a루트에 현재 단계의 마지막 접미사를 삽입해야 합니다.
이 작업을 수행하기 전에 두 가지 변수 ()를 추가로
도입했습니다. #물론 변수 는 항상 있었지만 지금까지는 사용하지 않았습니다.
- 활성 점 트리플이며
(active_node,active_edge,active_length)
- 는
remainder, 이는 우리가 삽입 할 필요가 얼마나 많은 새로운 접미사를 나타내는 정수
이 두 가지의 정확한 의미는 곧 명확 해 지겠지만 지금은 다음과 같이 말하겠습니다.
- 간단한에서
abc예를 들어, 활성 점은 언제나
(root,'\0x',0)즉 active_node, 루트 노드이었다 active_edge널 문자로 지정 '\0x'하고, active_length제로였다. 그 결과 모든 단계에서 삽입 한 새로운 모서리 하나가 새로 생성 된 모서리로 루트 노드에 삽입되었습니다. 이 정보를 나타 내기 위해 왜 트리플이 필요한지 곧 알게 될 것입니다.
- 는
remainder항상 각 단계의 시작 부분에 1로 설정했다. 이것의 의미는 각 단계의 끝에 적극적으로 삽입해야하는 접미사 수가 1 (항상 최종 문자 임)이라는 것입니다.
이제 이것은 변할 것입니다. 우리가 현재의 마지막 문자를 삽입 할 때 a루트에, 우리는 이미 시작으로 나가는 에지가있는 것을 알 a특히, : abca. 이러한 경우에 우리가하는 일은 다음과 같습니다.
- 우리 는
[4,#] 루트 노드에 새로운 가장자리 를 삽입 하지 않습니다 . 대신 접미사 a가 이미 트리에 있음을 알 수 있습니다. 그것은 더 긴 가장자리의 중간에서 끝나지 만 우리는 그것에 의해 방해받지 않습니다. 우리는 상황을 그대로 둡니다.
- 우리는 활성 점 설정 에를
(root,'a',1). 즉, 활성 지점이 루트 노드의 나가는 가장자리 중간에 a, 특히 해당 가장자리의 위치 1 뒤에 시작합니다 . 가장자리는 단순히 첫 번째 문자로 지정됩니다 a. 특정 문자로 시작하는 모서리 가 하나만 있을 수 있기 때문에 충분 합니다 (전체 설명을 읽은 후 이것이 사실인지 확인).
- 또한 증가
remainder하므로 다음 단계 시작시 2가됩니다.
관찰 : 삽입해야 할 마지막 접미사가 이미 트리에 존재하는 것으로 밝혀 지면 트리 자체는 전혀 변경되지 않습니다 (활성 지점 만 업데이트하고 및 remainder). 그러면 트리는 더 이상 현재 위치까지 접미사 트리를 정확하게 표현하지 않지만 모든 접미사를 포함 합니다 (최종 접미사 a가 암시 적 으로 포함 되기 때문에 ). 따라서 변수를 수정하는 것 (모두 고정 길이이므로 O (1) 임) 외에는 이 단계에서 수행 된 작업 이
없습니다 .
5 단계 : 현재 위치 #를 5로 업데이트합니다. 그러면 트리가 자동으로 다음과 같이 업데이트됩니다.

그리고 있기 때문에 remainder2 , 우리는 현재 위치의 마지막 두 접미사를 삽입해야 ab하고 b. 기본적으로 다음과 같은 이유 때문입니다.
a이전 단계 의 접미사가 제대로 삽입되지 않았습니다. 그래서 그것은 남아 있고, 우리가 한 걸음 진전 한 이래로 지금은에서 a로 성장 했습니다 ab.
- 그리고 우리는 새로운 최종 가장자리를 삽입해야합니다
b.
실제로 이것은 활성 지점 ( a현재 abcab가장자리의 뒤를 가리킴)으로 이동하여 현재 최종 문자를 삽입 함을 의미합니다 b. 그러나 : 다시 말하지만, b이미 같은 가장자리에 존재 한다는 것이 밝혀졌습니다 .
다시, 우리는 나무를 바꾸지 않습니다. 우리는 단순히 :
- 활성 포인트를로 업데이트합니다
(root,'a',2)(이전과 동일한 노드 및 에지이지만 이제는 뒤에 있음 b)
remainder이전 단계에서 최종 모서리를 올바르게 삽입하지 않았고 현재 최종 모서리도 삽입하지 않기 때문에를 3으로 늘 립니다.
명확하게하려면, 우리는 삽입했다 ab및 b현재 단계에 있지만, 때문에 ab이미 발견 된, 우리는 활성 지점을 업데이트하고도 삽입하지 않았다 b. 왜? ab이 트리에 있으면을 포함하여 모든 접미어도 트리에
b있어야합니다. 아마도 묵시적 일 수도 있지만, 지금까지 나무를 건축 한 방식 때문에 반드시 있어야합니다.
을 증가시켜 6 단계로 진행합니다 #. 트리가 자동으로 다음과 같이 업데이트됩니다.

remainder3 이므로 abx, bx및
을 삽입해야합니다 x. 활성 지점은 ab끝이 어디인지 알려주 므로 여기서 점프하고 삽입하면 x됩니다. 실제로 x아직 존재하지 않으므로 abcabx가장자리를 분할하고 내부 노드를 삽입합니다.

가장자리 표현은 여전히 텍스트에 대한 포인터이므로 내부 노드를 분할하고 삽입하는 것은 O (1) 시간 안에 수행 할 수 있습니다.
우리가 처리 한 그래서 abx및 감소 remainder(2)에 이제 우리는 다음 나머지 접미사를 삽입해야합니다 bx. 그러나이를 수행하기 전에 활성 지점을 업데이트해야합니다. 가장자리를 나누고 삽입 한 후의 규칙을 아래 규칙 1 이라고 하고 active_node루트가 될 때마다 적용됩니다
(다른 경우에는 규칙 3을 더 배우게됩니다). 다음은 규칙 1입니다.
루트에서 삽입 한 후
active_node 뿌리를 유지
active_edge 삽입해야 할 새 접미사의 첫 문자로 설정됩니다. b
active_length 1 씩 감소
따라서 새로운 활성 포인트 트리플 (root,'b',1)은 다음 인서트가 bcabx모서리, 1 문자 뒤, 즉 뒤에서 이루어져야 함을 나타냅니다 b. O (1) 시간에 삽입 점을 식별하고 x이미 존재 하는지 여부를 확인할 수 있습니다. 그것이 존재한다면, 우리는 현재 단계를 끝내고 모든 것을 그대로 둡니다. 그러나 x
존재하지 않으므로 가장자리를 분할하여 삽입합니다.

다시, 이것은 O (1) 시간이 걸렸고 우리 remainder는 1로 업데이트 하고 활성 포인트 (root,'x',0)는 규칙 1 상태로 업데이트 합니다 .
그러나 우리가해야 할 것이 하나 더 있습니다. 이 규칙을 2로 부릅니다 .
가장자리를 분할하고 새 노드를 삽입 하고 현재 단계에서 작성된 첫 번째 노드 가 아닌 경우 , 이전에 삽입 된 노드와 새 노드를 특수 포인터 인 접미사 링크를 통해 연결 합니다. 나중에 이것이 왜 유용한 지 알게 될 것입니다. 우리가 얻는 것은 다음과 같습니다. 접미사 링크는 점선으로 표시됩니다.

현재 단계의 최종 접미사를 계속 삽입해야합니다
x. active_length활성 노드 의 구성 요소가 0으로 떨어지기 때문에 최종 삽입은 루트에서 직접 이루어집니다. 로 시작하는 루트 노드에는 나가는 가장자리가 없으므로 x새로운 가장자리를 삽입합니다.

보시다시피, 현재 단계에서 나머지 모든 인서트가 만들어졌습니다.
= 7 을 설정 하여 7 단계 로 진행합니다.이 #문자 a는 항상 다음과 같이 모든 문자에 다음 문자를 자동으로 추가합니다
. 그런 다음 새로운 최종 문자를 활성 지점 (루트)에 삽입하고 이미있는 것을 찾습니다. 따라서 아무 것도 삽입하지 않고 현재 단계를 종료하고 활성 지점을로 업데이트합니다 (root,'a',1).
에서 8 단계 , #= 8, 우리는 추가 b하고, 이전에 볼 수 있듯이,이 유일한 수단은 우리가 활성을 가리킨 업데이트 (root,'a',2)및 증가 remainder하기 때문에, 다른 아무것도하지 않고이 b이미 존재합니다. 그러나 (O (1) 시간) 활성 지점이 이제 가장자리 끝에 있음을 알 수 있습니다. 이를로 재설정하여 반영합니다
(node1,'\0x',0). 여기서는 가장자리가 끝나는 node1내부 노드를 나타 ab냅니다.
그런 다음 = 9 단계# 에서 'c'를 삽입하면 최종 트릭을 이해하는 데 도움이됩니다.
두 번째 확장 : 접미사 링크 사용
항상 그렇듯이 #업데이트는 c리프 가장자리 에 자동으로 추가 되고 활성 지점으로 이동하여 'c'를 삽입 할 수 있는지 확인합니다. 'c'는 이미 그 가장자리에 존재하므로 활성 점을
(node1,'c',1) 증가 remainder시키고 다른 작업을 수행하지 않습니다.
이제 단계 #= 10 에서 remainder4는이므로 먼저 삽입하여
abcd(3 단계 전에 남아 있음) 삽입해야합니다.d 활성 지점 하여 ( 해야합니다.
d활성 지점 에 삽입하려고 하면 O (1) 시간에 에지 분할이 발생합니다.

active_node분할이 시작된로부터는, 적색, 상기에 표시된다. 마지막 규칙은 다음과 같습니다. 규칙 3 :
active_node루트 노드가 아닌 에지에서 에지를 분할 한 후 해당 노드에서 나오는 접미사 링크 (있는 경우) active_node를 따라 지정된 노드로 재설정합니다 . 접미사 링크가 없으면 active_node루트로 설정합니다 . active_edge
과active_length 변경되지 않습니다.
따라서 활성 지점은 이제 (node2,'c',1)이며 node2아래에 빨간색으로 표시됩니다.

삽입 abcd이 완료되었으므로 remainder3으로 감소 하고 현재 단계의 다음 나머지 접미사를 고려합니다
bcd. 규칙 3은 활성 지점을 올바른 노드와 가장자리로 설정 bcd하여 간단히 최종 문자를 삽입하여 삽입을 수행 할 수 있습니다.
d 활성 지점에 를 .
이렇게하면 또 다른 에지 분할이 발생하고 규칙 2 때문에 이전에 삽입 된 노드에서 새 노드로 접미사 링크를 작성해야합니다.

우리는 관찰 : 우리가 다음 할 수 있도록 접미사 링크는 활성 점을 다시 할 수있게 나머지 삽입 O (1)의 노력에. 위의 그래프를보고 실제로 레이블 ab의 노드가 노드 b의 접미사 abc에 연결되어 있고 노드 가에 연결되어 있는지 확인하십시오.
bc .
현재 단계가 아직 완료되지 않았습니다. remainder이제 2이고 규칙 3을 따라 활성 포인트를 다시 설정해야합니다. 현재 active_node(위의 빨간색) 접미사 링크가 없으므로 루트로 재설정합니다. 현재 포인트는(root,'c',1) 입니다.
따라서 다음 삽입 레이블을 시작으로 루트 노드의 나가는 가장자리에 발생합니다 c: cabxabcd첫 번째 문자, 즉 뒤에 뒤에 c. 이로 인해 또 다른 분할이 발생합니다.

여기에는 새로운 내부 노드 생성이 포함되므로 규칙 2를 따르고 이전에 만든 내부 노드에서 새 접미사 링크를 설정합니다.

( 이 작은 그래프에 Graphviz Dot 을 사용하고 있습니다. 새로운 접미사 링크로 인해 점이 기존 가장자리를 다시 정렬하게되었으므로 위에 삽입 된 유일한 것이 새로운 접미사 링크인지 확인하십시오.)
이것 remainder으로 1로 설정 될 수 있고 , active_node루트 이기 때문에 규칙 1을 사용하여 활성 포인트를로 업데이트합니다 (root,'d',0). 이것은 현재 단계의 마지막 삽입이 d
루트에서 하나를 삽입하는 것을 의미합니다 .

그게 마지막 단계 였고 우리는 끝났습니다. 그러나 여러 가지 최종 관측치 가 있습니다 .
각 단계에서 우리 #는 1 위치 앞으로 이동 합니다. O (1) 시간에 모든 리프 노드를 자동으로 업데이트합니다.
그러나 a) 이전 단계에서 남은 접미사 및 b) 현재 단계의 마지막 문자 하나를 다루지 않습니다 .
remainder몇 개의 추가 인서트가 필요한지 알려줍니다. 이 인서트는 현재 위치에서 끝나는 문자열의 마지막 접미사에 일대일로 대응합니다 #. 우리는 차례로 고려하고 삽입합니다. 중요 : 활성 지점은 정확히 어디로 가야하는지 알려주기 때문에 각 삽입은 O (1) 시간 안에 이루어지며 활성 지점에는 하나의 문자 만 추가하면됩니다. 왜? 다른 문자는 암시 적 으로 포함 되기 때문에
(그렇지 않으면 활성 지점이 원래 위치에 있지 않습니다).
이러한 각 삽입 후에 remainder접미사 링크가 있으면 줄이십시오. 그렇지 않다면 우리는 루트로 간다 (규칙 3). 루트에 이미있는 경우 규칙 1을 사용하여 활성 지점을 수정합니다. 어쨌든 O (1) 시간 만 걸립니다.
이러한 삽입 중 하나에서 삽입하려는 문자가 이미 존재하는 경우, remainder0 보다 크더라도 아무 것도하지 않고 현재 단계를 종료합니다 . 그 이유는 남아있는 인서트가 방금 만든 인서트의 접미사이기 때문입니다. 그러므로 그것들은 모두 현재 트리에 내재 되어 있습니다. 사실 remainder> 0 만든다 확실히 우리가 나중에 나머지 접미사 처리합니다.
알고리즘의 끝에서 remainder> 0이면 어떻게됩니까? 이것은 텍스트의 끝이 이전 어딘가에서 발생한 부분 문자열 일 때마다 해당됩니다. 이 경우 이전에 발생하지 않은 문자열 끝에 하나의 추가 문자를 추가해야합니다. 문헌에서 일반적으로 달러 기호 $는 그 기호 로 사용됩니다. 왜 중요한가요? -> 나중에 완성 된 접미사 트리를 사용하여 접미사를 검색하는 경우 리프에서 끝나는 경우에만 일치를 수락해야합니다 . 그렇지 않으면 트리에 주 문자열의 실제 접미사가 아닌 많은 문자열이 암시 적으로 포함되어 있기 때문에 많은 가짜 일치 가 발생합니다. 강제remainder마지막에 0이되는 것은 본질적으로 모든 접미사가 리프 노드에서 끝나는 것을 보장하는 방법입니다. 그러나 트리를 사용하여 기본 문자열의 접미사 뿐만 아니라 일반 하위 문자열 을 검색하려는 경우 아래 OP의 의견에서 제안한 것처럼이 마지막 단계는 실제로 필요하지 않습니다.
그렇다면 전체 알고리즘의 복잡성은 무엇입니까? 텍스트 길이가 n 자이면 분명히 n 단계가 있습니다 (또는 달러 기호를 추가하면 n + 1). 각 단계에서 변수를 업데이트하는 것 외에는 아무것도하지 않거나 remainderO (1) 시간이 걸리는 삽입을합니다. 이후는 remainder우리가 이전 단계에서 아무것도하지 횟수를 나타내며, 우리가 지금 만드는 것이 모든 삽입에 대한 감소, 우리가 뭔가를 할 시간의 총 수 (또는 n + 1) n은 정확히이다. 따라서 총 복잡도는 O (n)입니다.
그러나 내가 제대로 설명하지 않은 한 가지 작은 것이 있습니다. 접미사 링크를 따라 가고 활성 지점을 업데이트 한 다음 해당 active_length구성 요소가 new와 제대로 작동하지 않는 것을 발견 할 수 active_node있습니다. 예를 들어 다음과 같은 상황을 고려하십시오.

점선은 나머지 트리를 나타냅니다. 점선은 접미사 링크입니다.
이제 활성 지점을 으로하여 가장자리 (red,'d',3)뒤의 위치를 가리 킵니다 . 이제 필요한 업데이트를하고 접미사 링크를 따라 규칙 3에 따라 활성 지점을 업데이트한다고 가정합니다. 새 활성 지점은 입니다. 그러나 녹색 노드에서 나오는 가장자리는 이므로 2 자만 있습니다. 올바른 활성 지점을 찾으려면 해당 에지를 따라 파란색 노드로 이동하여로 재설정해야 합니다.fdefg(green,'d',3)dde(blue,'f',1)
특히 나쁜 경우는 active_length한 크게 할 수
remainder없음 한 크게 할 수있다. 올바른 활성 지점을 찾으려면 하나의 내부 노드를 뛰어 넘을 필요가있을뿐만 아니라 최악의 경우 최대 n 개까지 점프해야 할 수도 있습니다. 이는 각 단계 에서 일반적으로 O (n)이고 접미사 링크를 따른 후 활성 노드에 대한 사후 조정도 O (n) 일 수 있기 때문에 알고리즘에 숨겨진 O (n 2 ) 복잡성 이 있음을 의미합니까 remainder?
그 이유는 실제로 활성 포인트를 조정해야 할 경우 (예 : 위와 같이 녹색에서 파란색으로) 자체 접미사 링크가있는 새로운 노드로 연결되어 active_length줄어들 것입니다. 접미사 링크 체인을 따라 가면서 나머지 인서트를 만들고 active_length감소시킬 수 있으며 도중에 수행 할 수있는 활성 점 조정의 수는 active_length주어진 시간 보다 클 수 없습니다 . 이후는
active_length보다 클 수 없다 remainder, 그리고 remainder
뿐만 아니라 매 단계에서 O (n)은, 그러나 이제까지 만든 단위의 총 합이 remainder전체 프로세스의 과정은 O (N)가 너무 활성 점 조정의 수입니다 또한 O (n)에 의해 제한됩니다.