Sau đây là một nỗ lực để mô tả thuật toán Ukkonen bằng cách trước tiên hiển thị những gì nó làm khi chuỗi đơn giản (nghĩa là không chứa bất kỳ ký tự lặp lại nào), sau đó mở rộng nó sang thuật toán đầy đủ.
Đầu tiên, một vài tuyên bố sơ bộ.
Những gì chúng tôi đang xây dựng, về cơ bản giống như một bộ ba tìm kiếm. Vì vậy, có một nút gốc, các cạnh đi ra khỏi nó dẫn đến các nút mới và các cạnh khác đi ra khỏi các nút đó, v.v.
Nhưng : Không giống như trong bộ ba tìm kiếm, nhãn cạnh không phải là ký tự đơn. Thay vào đó, mỗi cạnh được dán nhãn bằng một cặp số nguyên
[from,to]
. Đây là những gợi ý vào văn bản. Theo nghĩa này, mỗi cạnh mang một nhãn chuỗi có độ dài tùy ý, nhưng chỉ chiếm không gian O (1) (hai con trỏ).
Nguyên tắc cơ bản
Trước tiên tôi muốn trình bày cách tạo cây hậu tố của một chuỗi đặc biệt đơn giản, một chuỗi không có các ký tự lặp lại:
abc
Thuật toán hoạt động theo các bước, từ trái sang phải . Có một bước cho mỗi ký tự của chuỗi . Mỗi bước có thể liên quan đến nhiều hơn một thao tác riêng lẻ, nhưng chúng ta sẽ thấy (xem các quan sát cuối cùng ở cuối) rằng tổng số thao tác là O (n).
Vì vậy, chúng tôi bắt đầu từ bên trái và đầu tiên chỉ chèn một ký tự đơn
a
bằng cách tạo một cạnh từ nút gốc (bên trái) sang một chiếc lá và gắn nhãn là [0,#]
, nghĩa là cạnh đại diện cho chuỗi con bắt đầu từ vị trí 0 và kết thúc ở cuối hiện tại . Tôi sử dụng ký hiệu #
để chỉ kết thúc hiện tại , ở vị trí 1 (ngay sau đó a
).
Vì vậy, chúng ta có một cây ban đầu, trông như thế này:
Và ý nghĩa của nó là đây:
Bây giờ chúng tôi tiến tới vị trí 2 (ngay sau đó b
). Mục tiêu của chúng tôi ở mỗi bước
là chèn tất cả các hậu tố lên đến vị trí hiện tại . Chúng tôi làm điều này bằng cách
- mở rộng
a
-edge hiện có đểab
- chèn một cạnh mới cho
b
Trong đại diện của chúng tôi, nó trông giống như
Và ý nghĩa của nó là:
Chúng tôi quan sát hai điều:
- Các đại diện cạnh cho
ab
là giống như nó đã từng là trong cây ban đầu: [0,#]
. Ý nghĩa của nó đã tự động thay đổi vì chúng tôi đã cập nhật vị trí hiện tại #
từ 1 lên 2.
- Mỗi cạnh tiêu thụ không gian O (1), bởi vì nó chỉ bao gồm hai con trỏ vào văn bản, bất kể nó đại diện cho bao nhiêu ký tự.
Tiếp theo, chúng tôi tăng vị trí một lần nữa và cập nhật cây bằng cách nối thêm c
vào mỗi cạnh hiện có và chèn một cạnh mới cho hậu tố mới c
.
Trong đại diện của chúng tôi, nó trông giống như
Và ý nghĩa của nó là:
Chúng tôi quan sát:
- Cây là cây hậu tố chính xác đến vị trí hiện tại
sau mỗi bước
- Có nhiều bước như có các ký tự trong văn bản
- Lượng công việc trong mỗi bước là O (1), bởi vì tất cả các cạnh hiện có được cập nhật tự động bằng cách tăng
#
và chèn một cạnh mới cho ký tự cuối cùng có thể được thực hiện trong thời gian O (1). Do đó đối với một chuỗi có độ dài n, chỉ cần thời gian O (n).
Phần mở rộng đầu tiên: Lặp lại đơn giản
Tất nhiên điều này hoạt động rất tốt chỉ bởi vì chuỗi của chúng tôi không chứa bất kỳ sự lặp lại. Bây giờ chúng ta xem xét một chuỗi thực tế hơn:
abcabxabcd
Nó bắt đầu abc
như trong ví dụ trước, sau đó ab
được lặp lại và theo sau x
, và sau đó abc
được lặp lại theo sau d
.
Bước 1 đến 3: Sau 3 bước đầu tiên, chúng ta có cây từ ví dụ trước:
Bước 4: Chúng tôi chuyển #
đến vị trí 4. Điều này ngầm cập nhật tất cả các cạnh hiện có vào đây:
và chúng ta cần chèn hậu tố cuối cùng của bước hiện tại a
, ở gốc.
Trước khi thực hiện điều này, chúng tôi giới thiệu thêm hai biến số (ngoài ra
#
), tất nhiên đã có ở đó mọi lúc nhưng chúng tôi chưa sử dụng chúng cho đến nay:
- Điểm hoạt động , là một bộ ba
(active_node,active_edge,active_length)
- Số
remainder
nguyên là số nguyên cho biết chúng ta cần chèn bao nhiêu hậu tố mới
Ý nghĩa chính xác của hai từ này sẽ sớm trở nên rõ ràng, nhưng bây giờ chúng ta hãy nói:
- Trong
abc
ví dụ đơn giản , điểm hoạt động luôn luôn
(root,'\0x',0)
, tức active_node
là nút gốc, active_edge
được chỉ định là ký tự null '\0x'
và active_length
bằng không. Hiệu quả của việc này là một cạnh mới mà chúng ta đã chèn trong mỗi bước được chèn vào nút gốc dưới dạng cạnh mới được tạo. Chúng tôi sẽ sớm thấy lý do tại sao một bộ ba là cần thiết để đại diện cho thông tin này.
- Các
remainder
luôn thiết lập để 1 lúc bắt đầu của mỗi bước. Ý nghĩa của việc này là số lượng hậu tố chúng ta phải chủ động chèn vào cuối mỗi bước là 1 (luôn luôn chỉ là ký tự cuối cùng).
Bây giờ điều này sẽ thay đổi. Khi chúng ta chèn ký tự cuối cùng hiện a
tại vào thư mục gốc, chúng tôi nhận thấy rằng đã có một cạnh ra bắt đầu bằng a
, cụ thể là : abca
. Dưới đây là những gì chúng tôi làm trong trường hợp như vậy:
- Chúng tôi không chèn một cạnh mới
[4,#]
vào nút gốc. Thay vào đó chúng tôi chỉ đơn giản nhận thấy rằng hậu tố a
đã có trong cây của chúng tôi. Nó kết thúc ở giữa một cạnh dài hơn, nhưng chúng tôi không bị làm phiền bởi điều đó. Chúng tôi chỉ để mọi thứ theo cách họ đang có.
- Chúng tôi đặt điểm hoạt động thành
(root,'a',1)
. Điều đó có nghĩa là điểm hoạt động hiện đang ở đâu đó ở giữa cạnh ra của nút gốc bắt đầu bằng a
, cụ thể, sau vị trí 1 trên cạnh đó. Chúng tôi nhận thấy rằng cạnh được chỉ định đơn giản bởi ký tự đầu tiên của nó a
. Điều đó đủ bởi vì chỉ có thể có một cạnh bắt đầu với bất kỳ ký tự cụ thể nào (xác nhận rằng điều này là đúng sau khi đọc qua toàn bộ mô tả).
- Chúng tôi cũng tăng
remainder
, vì vậy ở đầu bước tiếp theo sẽ là 2.
Quan sát: Khi hậu tố cuối cùng chúng ta cần chèn đã tồn tại trong cây , bản thân cây không bị thay đổi (chúng tôi chỉ cập nhật điểm hoạt động và remainder
). Cây sau đó không phải là một đại diện chính xác của cây hậu tố cho đến vị trí hiện tại nữa, nhưng nó chứa tất cả các hậu tố (vì hậu tố cuối cùng a
được chứa ẩn ). Do đó, ngoài việc cập nhật các biến (tất cả đều có độ dài cố định, vì vậy đây là O (1)),
không có công việc nào được thực hiện trong bước này.
Bước 5: Chúng tôi cập nhật vị trí hiện tại #
lên 5. Điều này sẽ tự động cập nhật cây thành này:
Và bởi vì remainder
là 2 , chúng ta cần chèn hai hậu tố cuối cùng của vị trí hiện tại: ab
và b
. Điều này về cơ bản là vì:
- Các
a
hậu tố từ bước trước chưa bao giờ được lắp đúng. Vì vậy, nó đã ở lại , và kể từ khi chúng tôi đã tiến một bước, bây giờ nó đã phát triển từ a
đến ab
.
- Và chúng ta cần chèn cạnh cuối cùng mới
b
.
Trong thực tế, điều này có nghĩa là chúng ta đi đến điểm hoạt động (chỉ ra phía sau điểm a
hiện tại là abcab
cạnh) và chèn ký tự cuối cùng hiện tại b
. Nhưng: Một lần nữa, hóa ra b
cũng đã có mặt trên cùng một cạnh.
Vì vậy, một lần nữa, chúng tôi không thay đổi cây. Chúng tôi chỉ đơn giản là:
- Cập nhật điểm hoạt động thành
(root,'a',2)
(cùng nút và cạnh như trước đây, nhưng bây giờ chúng tôi trỏ đến phía sau b
)
- Tăng
remainder
lên 3 vì chúng tôi vẫn chưa chèn đúng cạnh cuối cùng từ bước trước và chúng tôi cũng không chèn cạnh cuối cùng hiện tại.
Để rõ ràng: Chúng tôi đã phải chèn ab
và b
trong bước hiện tại, nhưng vì ab
đã được tìm thấy, chúng tôi đã cập nhật điểm hoạt động và thậm chí không cố gắng chèn b
. Tại sao? Bởi vì nếu ab
ở trong cây,
mọi hậu tố của nó (bao gồm b
) cũng phải ở trong cây. Có lẽ chỉ ngầm , nhưng nó phải ở đó, vì cách chúng tôi đã xây dựng cây cho đến nay.
Chúng tôi tiến hành bước 6 bằng cách tăng dần #
. Cây được tự động cập nhật thành:
Bởi vì remainder
là 3 , chúng ta phải chèn abx
, bx
và
x
. Điểm hoạt động cho chúng ta biết nơi ab
kết thúc, vì vậy chúng ta chỉ cần nhảy đến đó và chèn x
. Thật vậy, x
vẫn chưa có, vì vậy chúng tôi chia abcabx
cạnh và chèn một nút bên trong:
Các biểu diễn cạnh vẫn là các con trỏ vào văn bản, do đó việc chia và chèn một nút bên trong có thể được thực hiện trong thời gian O (1).
Vì vậy, chúng tôi đã xử lý abx
và giảm remainder
xuống còn 2. Bây giờ chúng tôi cần chèn hậu tố còn lại tiếp theo , bx
. Nhưng trước khi làm điều đó, chúng ta cần cập nhật điểm hoạt động. Quy tắc này, sau khi tách và chèn một cạnh, sẽ được gọi là Quy tắc 1 bên dưới và áp dụng bất cứ khi nào
active_node
là root (chúng ta sẽ tìm hiểu quy tắc 3 cho các trường hợp khác bên dưới). Đây là quy tắc 1:
Sau khi chèn từ gốc,
active_node
vẫn còn root
active_edge
được đặt thành ký tự đầu tiên của hậu tố mới mà chúng ta cần chèn, nghĩa là b
active_length
giảm 1
Do đó, bộ ba điểm hoạt động mới (root,'b',1)
chỉ ra rằng lần chèn tiếp theo phải được thực hiện ở bcabx
cạnh, phía sau 1 ký tự, tức là phía sau b
. Chúng ta có thể xác định điểm chèn trong thời gian O (1) và kiểm tra xem x
đã có mặt hay chưa. Nếu nó có mặt, chúng tôi sẽ kết thúc bước hiện tại và để mọi thứ vẫn như cũ. Nhưng x
không có mặt, vì vậy chúng tôi chèn nó bằng cách tách cạnh:
Một lần nữa, điều này mất thời gian O (1) và chúng tôi cập nhật remainder
lên 1 và điểm hoạt động thành (root,'x',0)
trạng thái quy tắc 1.
Nhưng có một điều nữa chúng ta cần làm. Chúng tôi sẽ gọi Quy tắc 2 này:
Nếu chúng ta tách một cạnh và chèn một nút mới và nếu đó không phải là nút đầu tiên được tạo trong bước hiện tại, chúng ta sẽ kết nối nút được chèn trước đó và nút mới thông qua một con trỏ đặc biệt, một liên kết hậu tố . Sau này chúng ta sẽ thấy tại sao điều đó hữu ích. Đây là những gì chúng ta nhận được, liên kết hậu tố được biểu diễn dưới dạng một cạnh chấm:
Chúng ta vẫn cần chèn hậu tố cuối cùng của bước hiện tại ,
x
. Vì active_length
thành phần của nút hoạt động đã giảm xuống 0, nên lần chèn cuối cùng được thực hiện trực tiếp tại thư mục gốc. Vì không có cạnh ra ở nút gốc bắt đầu bằng x
, chúng tôi chèn một cạnh mới:
Như chúng ta có thể thấy, trong bước hiện tại, tất cả các phần chèn còn lại đã được thực hiện.
Chúng tôi tiến hành bước 7 bằng cách đặt #
= 7, tự động nối thêm ký tự tiếp theo
a
, vào tất cả các cạnh của lá, như mọi khi. Sau đó, chúng tôi cố gắng chèn ký tự cuối cùng mới vào điểm hoạt động (gốc) và thấy rằng nó đã ở đó. Vì vậy, chúng tôi kết thúc bước hiện tại mà không chèn bất cứ điều gì và cập nhật điểm hoạt động (root,'a',1)
.
Trong bước 8 , #
= 8, chúng tôi nối thêm b
và như đã thấy trước đây, điều này chỉ có nghĩa là chúng tôi cập nhật điểm hoạt động (root,'a',2)
và tăng remainder
mà không làm gì khác, vì b
đã có sẵn. Tuy nhiên, chúng tôi nhận thấy (trong O (1) thời gian) rằng điểm hoạt động hiện ở cuối một cạnh. Chúng tôi phản ánh điều này bằng cách thiết lập lại nó
(node1,'\0x',0)
. Ở đây, tôi sử dụng node1
để chỉ nút nội bộ mà ab
cạnh kết thúc tại.
Sau đó, trong bước #
= 9 , chúng ta cần chèn 'c' và điều này sẽ giúp chúng ta hiểu được mẹo cuối cùng:
Tiện ích mở rộng thứ hai: Sử dụng liên kết hậu tố
Như mọi khi, #
bản cập nhật c
sẽ tự động thêm vào các cạnh lá và chúng tôi đi đến điểm hoạt động để xem liệu chúng tôi có thể chèn 'c' không. Hóa ra 'c' đã tồn tại ở cạnh đó, vì vậy chúng tôi đặt điểm hoạt động thành
(node1,'c',1)
, tăng remainder
và không làm gì khác.
Bây giờ trong bước #
= 10 , remainder
là 4, và vì vậy trước tiên chúng ta cần chèn
abcd
(vẫn còn từ 3 bước trước) bằng cách chèn d
tại điểm hoạt động.
Cố gắng chèn d
tại điểm hoạt động sẽ gây ra sự phân tách cạnh trong thời gian O (1):
Các active_node
, từ đó phân chia được khởi xướng, được đánh dấu màu đỏ ở trên. Đây là quy tắc cuối cùng, Quy tắc 3:
Sau khi tách một cạnh từ một active_node
nút không phải là nút gốc, chúng ta theo liên kết hậu tố đi ra khỏi nút đó, nếu có, và đặt lại active_node
nút cho nút mà nó trỏ tới. Nếu không có liên kết hậu tố, chúng tôi đặt active_node
gốc. active_edge
và active_length
vẫn không thay đổi.
Vì vậy, điểm hoạt động là ngay bây giờ (node2,'c',1)
và node2
được đánh dấu màu đỏ bên dưới:
Vì việc chèn abcd
hoàn tất, chúng tôi giảm remainder
xuống còn 3 và xem xét hậu tố tiếp theo còn lại của bước hiện tại ,
bcd
. Quy tắc 3 đã đặt điểm hoạt động thành chỉ nút và cạnh phải để việc chèn bcd
có thể được thực hiện bằng cách chỉ cần chèn ký tự cuối cùng của nó
d
tại điểm hoạt động.
Làm điều này gây ra sự phân tách cạnh khác và vì quy tắc 2 , chúng ta phải tạo một liên kết hậu tố từ nút được chèn trước đó đến nút mới:
Chúng tôi quan sát: Liên kết Suffix cho phép chúng tôi đặt lại điểm hoạt động để chúng tôi có thể thực hiện thao tác chèn tiếp theo với nỗ lực O (1). Nhìn vào biểu đồ trên để xác nhận rằng thực sự nút tại nhãn ab
được liên kết với nút tại b
(hậu tố của nó) và nút tại abc
được liên kết đến
bc
.
Bước hiện tại vẫn chưa kết thúc. remainder
bây giờ là 2 và chúng ta cần tuân theo quy tắc 3 để thiết lập lại điểm hoạt động. Vì hiện tại active_node
(màu đỏ ở trên) không có liên kết hậu tố, chúng tôi đặt lại để root. Các điểm hoạt động là bây giờ (root,'c',1)
.
Do đó, lần chèn tiếp theo xảy ra ở một cạnh đi ra của nút gốc có nhãn bắt đầu bằng c
: cabxabcd
, đằng sau ký tự đầu tiên, tức là phía sau c
. Điều này gây ra sự chia rẽ khác:
Và vì điều này liên quan đến việc tạo một nút nội bộ mới, chúng tôi tuân theo quy tắc 2 và đặt liên kết hậu tố mới từ nút nội bộ được tạo trước đó:
(Tôi đang sử dụng Graphviz Dot cho các đồ thị nhỏ. Các liên kết hậu tố mới gây ra dấu chấm để sắp xếp lại các cạnh hiện có, nên kiểm tra cẩn thận để xác nhận rằng điều duy nhất đã được chèn trên là một liên kết hậu tố mới.)
Với điều này, remainder
có thể được đặt thành 1 và vì active_node
là root, chúng tôi sử dụng quy tắc 1 để cập nhật điểm hoạt động thành (root,'d',0)
. Điều này có nghĩa là lần chèn cuối cùng của bước hiện tại là chèn một lệnh d
gốc:
Đó là bước cuối cùng và chúng tôi đã hoàn thành. Có một số quan sát cuối cùng , mặc dù:
Trong mỗi bước chúng ta tiến #
lên 1 vị trí. Điều này tự động cập nhật tất cả các nút lá trong thời gian O (1).
Nhưng nó không xử lý a) bất kỳ hậu tố nào còn lại từ các bước trước và b) với một ký tự cuối cùng của bước hiện tại.
remainder
cho chúng tôi biết có bao nhiêu chèn thêm mà chúng tôi cần phải thực hiện. Các chèn này tương ứng một-một với các hậu tố cuối cùng của chuỗi kết thúc tại vị trí hiện tại #
. Chúng tôi xem xét từng cái một và thực hiện chèn. Quan trọng: Mỗi lần chèn được thực hiện trong thời gian O (1) vì điểm hoạt động cho chúng ta biết chính xác nơi cần đi và chúng ta chỉ cần thêm một ký tự duy nhất tại điểm hoạt động. Tại sao? Bởi vì các ký tự khác được chứa ẩn
(nếu không thì điểm hoạt động sẽ không ở vị trí của nó).
Sau mỗi lần chèn như vậy, chúng tôi giảm dần remainder
và theo liên kết hậu tố nếu có. Nếu không chúng ta đi root (quy tắc 3). Nếu chúng tôi đã root, chúng tôi sửa đổi điểm hoạt động bằng quy tắc 1. Trong mọi trường hợp, chỉ mất O (1) thời gian.
Nếu, trong một trong những lần chèn này, chúng tôi thấy rằng ký tự chúng tôi muốn chèn đã ở đó, chúng tôi không làm gì cả và kết thúc bước hiện tại, ngay cả khi remainder
> 0. Lý do là bất kỳ phần chèn nào còn lại sẽ là hậu tố của cái mà chúng ta vừa thử thực hiện. Do đó tất cả chúng đều ẩn trong cây hiện tại. Thực tế là remainder
> 0 đảm bảo chúng ta xử lý các hậu tố còn lại sau này.
Nếu ở cuối thuật toán remainder
> 0 thì sao? Đây sẽ là trường hợp bất cứ khi nào phần cuối của văn bản là một chuỗi con xảy ra ở đâu đó trước đó. Trong trường hợp đó, chúng ta phải nối thêm một ký tự ở cuối chuỗi chưa xảy ra trước đó. Trong các tài liệu, thông thường ký hiệu đô la $
được sử dụng như một biểu tượng cho điều đó. Tại sao lại là vấn đề đó? -> Nếu sau này chúng ta sử dụng cây hậu tố đã hoàn thành để tìm kiếm hậu tố, chúng ta phải chấp nhận kết quả khớp chỉ khi chúng kết thúc tại một chiếc lá . Nếu không, chúng ta sẽ nhận được rất nhiều kết quả trùng khớp, bởi vì có nhiều chuỗi ẩn trong cây không phải là hậu tố thực sự của chuỗi chính. Buộcremainder
về 0 ở cuối về cơ bản là một cách để đảm bảo rằng tất cả các hậu tố kết thúc tại một nút lá. Tuy nhiên, nếu chúng ta muốn sử dụng cây để tìm kiếm các chuỗi con chung , không chỉ các hậu tố của chuỗi chính, bước cuối cùng này thực sự không bắt buộc, như đề xuất của bình luận của OP dưới đây.
Vậy độ phức tạp của toàn bộ thuật toán là gì? Nếu văn bản có độ dài n ký tự, rõ ràng có n bước (hoặc n + 1 nếu chúng ta thêm ký hiệu đô la). Trong mỗi bước, chúng tôi sẽ không làm gì cả (ngoài việc cập nhật các biến) hoặc chúng tôi thực hiện remainder
chèn, mỗi lần lấy O (1) thời gian. Vì remainder
cho biết số lần chúng tôi không làm gì trong các bước trước đó và được giảm cho mỗi lần chèn mà chúng tôi thực hiện bây giờ, tổng số lần chúng tôi làm một cái gì đó chính xác là n (hoặc n + 1). Do đó, tổng độ phức tạp là O (n).
Tuy nhiên, có một điều nhỏ mà tôi đã không giải thích chính xác: Có thể xảy ra việc chúng ta theo một liên kết hậu tố, cập nhật điểm hoạt động và sau đó thấy rằng active_length
thành phần của nó không hoạt động tốt với cái mới active_node
. Ví dụ, hãy xem xét một tình huống như thế này:
(Các đường đứt nét chỉ phần còn lại của cây. Đường chấm chấm là một liên kết hậu tố.)
Bây giờ chúng ta hãy điểm hoạt động được (red,'d',3)
, vì vậy nó trỏ đến nơi phía sau f
trên defg
mép. Bây giờ giả sử chúng tôi đã thực hiện các cập nhật cần thiết và bây giờ theo liên kết hậu tố để cập nhật điểm hoạt động theo quy tắc 3. Điểm hoạt động mới là (green,'d',3)
. Tuy nhiên, d
-edge đi ra khỏi nút màu xanh lá cây de
, vì vậy nó chỉ có 2 ký tự. Để tìm điểm hoạt động chính xác, rõ ràng chúng ta cần phải theo cạnh đó đến nút màu xanh và đặt lại (blue,'f',1)
.
Trong trường hợp đặc biệt xấu, active_length
có thể lớn bằng
remainder
, có thể lớn bằng n. Và rất có thể xảy ra rằng để tìm ra điểm hoạt động chính xác, chúng ta không chỉ cần nhảy qua một nút bên trong, mà có lẽ nhiều, đến n trong trường hợp xấu nhất. Điều đó có nghĩa là thuật toán có độ phức tạp O (n 2 ) ẩn, bởi vì trong mỗi bước remainder
thường là O (n) và các điều chỉnh sau cho nút hoạt động sau khi theo liên kết hậu tố cũng có thể là O (n)?
Không. Lý do là nếu thực sự chúng ta phải điều chỉnh điểm hoạt động (ví dụ từ màu xanh sang màu xanh như trên), điều đó đưa chúng ta đến một nút mới có liên kết hậu tố riêng và active_length
sẽ bị giảm. Khi chúng tôi theo dõi chuỗi liên kết hậu tố, chúng tôi thực hiện các lần chèn còn lại, active_length
chỉ có thể giảm và số lần điều chỉnh điểm hoạt động mà chúng tôi có thể thực hiện trên đường không thể lớn hơn active_length
bất kỳ lúc nào. Vì
active_length
không bao giờ có thể lớn hơn remainder
và remainder
O (n) không chỉ trong mỗi bước, mà tổng số gia tăng từng được thực hiện remainder
trong toàn bộ quá trình là O (n), số lần điều chỉnh điểm hoạt động là cũng giới hạn bởi O (n).