ต่อไปนี้เป็นความพยายามที่จะอธิบายอัลกอริธึม Ukkonen โดยการแสดงสิ่งที่ทำก่อนเมื่อสตริงนั้นง่าย (เช่นไม่มีตัวอักษรซ้ำ ๆ ) แล้วขยายไปยังอัลกอริธึมเต็ม
ก่อนแถลงการณ์เบื้องต้นบางประการ
สิ่งที่เรากำลังสร้างเป็นพื้นเช่น Trie ค้นหา ดังนั้นจึงมีโหนดรูทซึ่งมีขอบออกไปจากนั้นนำไปสู่โหนดใหม่และขอบต่อไปจะออกจากโหนดเหล่านั้นเป็นต้น
แต่ : แตกต่างจากใน Trie การค้นหาเลเบลขอบไม่ใช่อักขระเดี่ยว [from,to]
แต่ขอบแต่ละจะมีป้ายโดยใช้คู่ของจำนวนเต็ม
นี่คือพอยน์เตอร์ในข้อความ ในแง่นี้แต่ละขอบมีฉลากสตริงที่มีความยาวตามอำเภอใจ แต่ใช้พื้นที่ O (1) (สองพอยน์เตอร์) เท่านั้น
หลักการพื้นฐาน
ฉันต้องการแสดงให้เห็นถึงวิธีการสร้างทรีต่อท้ายของสตริงที่ง่ายโดยเฉพาะอย่างยิ่งสตริงที่ไม่มีอักขระซ้ำ:
abc
ขั้นตอนวิธีการทำงานในขั้นตอนจากซ้ายไปขวา มีขั้นตอนหนึ่งสำหรับตัวอักษรของสตริงทุก แต่ละขั้นตอนอาจเกี่ยวข้องกับการดำเนินการมากกว่าหนึ่งอย่าง แต่เราจะเห็น (ดูการสังเกตสุดท้ายในตอนท้าย) ว่าจำนวนการดำเนินการทั้งหมดคือ O (n)
ดังนั้นเราเริ่มจากด้านซ้ายและแรกแทรกเฉพาะอักขระเดียว
a
โดยสร้างขอบจากโหนดรูท (ด้านซ้าย) ไปยังใบไม้และติดป้ายเป็น[0,#]
ซึ่งหมายความว่าขอบแสดงสตริงย่อยเริ่มต้นที่ตำแหน่ง 0 และสิ้นสุด ที่ปลายปัจจุบัน ฉันใช้สัญลักษณ์#
เพื่อหมายถึงจุดจบปัจจุบันซึ่งอยู่ที่ตำแหน่ง 1 (ทันทีหลังจากa
)
ดังนั้นเราจึงมีต้นไม้เริ่มต้นซึ่งมีลักษณะดังนี้:
และมันหมายถึงอะไร:
ตอนนี้เรากำลังไปสู่ตำแหน่งที่ 2 (หลังจากนั้นb
) เป้าหมายของเราในแต่ละขั้นตอน
คือการแทรกคำต่อท้ายทั้งหมดขึ้นอยู่กับตำแหน่งปัจจุบัน เราทำสิ่งนี้โดย
- ขยายขอบเขตที่มีอยู่
a
ไปเป็นab
- แทรกหนึ่งขอบใหม่สำหรับ
b
ในการเป็นตัวแทนของเราดูเหมือนว่า
และมันหมายถึงอะไร:
เราสังเกตสองสิ่ง:
- การแทนค่าขอบ
ab
นั้นเหมือนกับที่เคยมีในแผนผังเริ่มต้น: [0,#]
. ความหมายเปลี่ยนไปโดยอัตโนมัติเนื่องจากเราอัปเดตตำแหน่งปัจจุบัน#
จาก 1 เป็น 2
- ขอบแต่ละอันใช้พื้นที่ O (1) เนื่องจากมันประกอบด้วยพอยน์เตอร์เพียงสองตัวเท่านั้นลงในข้อความโดยไม่คำนึงถึงจำนวนตัวอักษรที่แสดง
ต่อไปเราจะเพิ่มตำแหน่งอีกครั้งและอัปเดตต้นไม้โดยผนวกทุกขอบที่มีอยู่และการใส่ขอบใหม่สำหรับปัจจัยใหม่c
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)
- The
remainder
ซึ่งเป็นจำนวนเต็มที่ระบุจำนวนคำต่อท้ายใหม่ที่เราต้องการแทรก
ความหมายที่แท้จริงของทั้งสองนี้จะชัดเจนในไม่ช้า แต่สำหรับตอนนี้ขอพูดว่า:
- ใน
abc
ตัวอย่างง่าย ๆจุดที่ใช้งานอยู่เสมอ
(root,'\0x',0)
คือactive_node
โหนดรูตactive_edge
ถูกระบุเป็นอักขระ null '\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 ซึ่งจะอัปเดตทรีเป็นแบบนี้โดยอัตโนมัติ:
และเพราะremainder
เป็น 2เราต้องใส่สองต่อท้ายสุดท้ายของตำแหน่งปัจจุบัน: และab
b
นี่เป็นเพราะ:
a
ต่อท้ายจากขั้นตอนก่อนหน้านี้ไม่เคยมีการใส่อย่างถูกต้อง ดังนั้นจึงยังคงอยู่และเนื่องจากเรามีความก้าวหน้าในขั้นตอนเดียวจะได้เติบโตขึ้นในขณะนี้จากการa
ab
b
และเราจำเป็นต้องใส่ขอบสุดท้ายใหม่
ในทางปฏิบัติที่นี้หมายถึงว่าเราจะไปถึงจุดที่ใช้งาน (ซึ่งจุดที่จะอยู่เบื้องหลังa
ในตอนนี้คืออะไรabcab
ขอบ) b
และใส่ตัวอักษรตัวสุดท้ายในปัจจุบัน แต่:อีกครั้งปรากฎว่าb
มีอยู่บนขอบเดียวกันนั้นแล้ว
ดังนั้นอีกครั้งเราจะไม่เปลี่ยนต้นไม้ เราเพียง:
- อัปเดตจุดที่ใช้งานเป็น
(root,'a',2)
(โหนดและขอบเดียวกันเหมือน แต่ก่อนตอนนี้เราชี้ไปด้านหลังb
)
- เพิ่มค่า
remainder
เป็น 3 เนื่องจากเรายังไม่ได้แทรกขอบสุดท้ายจากขั้นตอนก่อนหน้านี้อย่างเหมาะสมและเราไม่ได้แทรกขอบสุดท้ายในปัจจุบัน
ต้องมีความชัดเจน: เรามีการแทรกab
และb
ในขั้นตอนปัจจุบัน แต่เพราะถูกพบแล้วเรามีการปรับปรุงจุดที่ใช้งานและไม่ได้พยายามที่จะแทรกab
b
ทำไม? เพราะถ้าab
อยู่ในต้นไม้
คำต่อท้ายทุกอันของมัน (รวมถึงb
) จะต้องอยู่ในต้นไม้ด้วย บางทีอาจจะเป็นเพียงนัยแต่มันต้องอยู่ที่นั่นเพราะวิธีที่เราสร้างต้นไม้จนถึงตอนนี้
เราดำเนินการขั้นตอนที่ 6#
โดยการเพิ่ม ต้นไม้ถูกอัพเดตเป็น:
เพราะremainder
เป็น 3เราจะต้องใส่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 , remainder
4 และเพื่อให้เราต้องแทรก
abcd
(ซึ่งยังคงอยู่จาก 3 ขั้นตอนที่แล้ว) โดยการใส่d
ที่จุดที่ใช้งานอยู่
การพยายามแทรกd
ที่จุดที่ใช้งานอยู่จะทำให้เกิดการแตกขอบในเวลา O (1):
การactive_node
เริ่มต้นการแยกนั้นทำเครื่องหมายด้วยสีแดงด้านบน นี่คือกฎสุดท้ายกฎข้อ 3:
หลังจากแยกขอบจากactive_node
ที่ไม่ใช่โหนดรูทแล้วเราจะตามลิงก์ต่อท้ายที่ออกจากโหนดนั้นหากมีและตั้งค่าactive_node
เป็นโหนดที่ชี้ไป หากไม่มีลิงก์คำต่อท้ายเราจะตั้งค่าactive_node
เป็นรูท active_edge
และactive_length
ยังคงไม่เปลี่ยนแปลง
ดังนั้นจุดที่ใช้งานอยู่ในขณะนี้(node2,'c',1)
และnode2
ถูกทำเครื่องหมายด้วยสีแดงด้านล่าง:
ตั้งแต่การแทรกของabcd
เสร็จแล้วเราพร่องremainder
3
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)
แต่จะไม่จัดการกับ) ส่วนต่อท้ายที่เหลือจากขั้นตอนก่อนหน้าและ b) กับอักขระสุดท้ายหนึ่งตัวของขั้นตอนปัจจุบัน
remainder
บอกเราว่ามีเม็ดมีดเพิ่มเติมกี่อันที่เราต้องทำ #
แทรกเหล่านี้สอดคล้องแบบหนึ่งต่อหนึ่งไปต่อท้ายสุดท้ายของสตริงที่ปลายที่ตำแหน่งปัจจุบัน เราจะพิจารณาอย่างใดอย่างหนึ่งหลังจากที่อื่นและทำการแทรก สำคัญ: การแทรกแต่ละครั้งจะเสร็จสิ้นในเวลา O (1) เนื่องจากจุดที่ใช้งานบอกเราว่าจะไปที่ไหนและเราจำเป็นต้องเพิ่มตัวละครเดียวที่จุดที่ใช้งานอยู่ ทำไม? เพราะตัวละครอื่น ๆ มีอยู่โดยปริยาย
(มิฉะนั้นจุดที่ใช้งานจะไม่อยู่ที่มันอยู่)
หลังจากการแทรกแต่ละครั้งเราจะลดค่าremainder
และติดตามลิงก์คำต่อท้ายหากมี ถ้าไม่ใช่เราไปรูท (กฎ 3) หากเราอยู่ที่รูทแล้วเราจะแก้ไขจุดที่ใช้งานโดยใช้กฎ 1 ในกรณีใด ๆ มันต้องใช้เวลา O (1) เท่านั้น
หากในช่วงหนึ่งของการแทรกเหล่านี้เราพบว่าตัวละครที่เราต้องการแทรกมีอยู่แล้วเราไม่ได้ทำอะไรและจบขั้นตอนปัจจุบันแม้ว่าremainder
> 0 เหตุผลก็คือเม็ดมีดที่เหลืออยู่นั้นจะเป็นคำต่อท้ายของเม็ดมีดที่เราพยายามทำ ดังนั้นพวกเขาทั้งหมดโดยปริยายในโครงสร้างปัจจุบัน ความจริงที่remainder
> 0 ทำให้แน่ใจว่าเราจัดการกับส่วนต่อท้ายที่เหลือในภายหลัง
เกิดอะไรขึ้นถ้าในตอนท้ายของอัลกอริทึมremainder
> 0? กรณีนี้จะเกิดขึ้นเมื่อใดก็ตามที่จุดสิ้นสุดของข้อความเป็นซับสตริงที่เกิดขึ้นที่ใดที่หนึ่งก่อนหน้านี้ ในกรณีนั้นเราจะต้องผนวกอักขระพิเศษหนึ่งตัวที่ส่วนท้ายของสตริงที่ไม่เคยเกิดขึ้นมาก่อน ในวรรณคดีมักจะใช้สัญลักษณ์ดอลลาร์$
เป็นสัญลักษณ์สำหรับสิ่งนั้น ทำไมถึงเป็นเช่นนั้น? -> หากต่อมาเราใช้คำต่อท้ายต้นไม้เสร็จสิ้นการค้นหาคำต่อท้ายเราต้องยอมรับการแข่งขันเท่านั้นหากพวกเขาสิ้นสุดที่ใบ มิฉะนั้นเราจะได้รับจำนวนมากของการแข่งขันปลอมเพราะมีหลายสายโดยปริยายที่มีอยู่ในต้นไม้ที่ไม่ได้เกิดขึ้นจริงของคำต่อท้ายสตริงหลัก พระเดชremainder
เป็น 0 ในตอนท้ายเป็นหลักเป็นวิธีการเพื่อให้แน่ใจว่าต่อท้ายทั้งหมดจบที่โหนดใบ อย่างไรก็ตามหากเราต้องการใช้แผนผังเพื่อค้นหาสตริงย่อยทั่วไปไม่เพียง แต่คำต่อท้ายของสตริงหลักขั้นตอนสุดท้ายนี้ไม่จำเป็นต้องทำตามที่แนะนำโดยความคิดเห็นของ OP ด้านล่าง
ดังนั้นความซับซ้อนของอัลกอริธึมทั้งหมดคืออะไร? หากข้อความมีความยาวอักขระ n ตัวจะเห็นได้ชัดว่ามีขั้นตอน n ขั้น (หรือ n + 1 ถ้าเราเพิ่มเครื่องหมายดอลลาร์) ในแต่ละขั้นตอนเราจะไม่ทำอะไรเลย (นอกจากการปรับปรุงตัวแปร) หรือเราสร้างremainder
ส่วนแทรกโดยแต่ละครั้งใช้เวลา O (1) เนื่องจากremainder
ระบุจำนวนครั้งที่เราไม่ได้ทำอะไรในขั้นตอนก่อนหน้านี้และจะลดลงสำหรับทุกส่วนแทรกที่เราทำในตอนนี้จำนวนครั้งทั้งหมดที่เราทำบางอย่างคือ n (หรือ n + 1) ดังนั้นความซับซ้อนทั้งหมดคือ O (n)
แต่มีสิ่งหนึ่งที่เล็ก ๆ ที่ผมไม่ต้องอธิบาย: มันสามารถเกิดขึ้นว่าเราทำตามลิงค์ต่อท้ายปรับปรุงจุดที่ใช้งานแล้วพบว่าองค์ประกอบที่ไม่ทำงานได้ดีกับใหม่active_length
active_node
ตัวอย่างเช่นพิจารณาสถานการณ์เช่นนี้:
(เส้นประบ่งบอกถึงส่วนที่เหลือของต้นไม้เส้นประเป็นลิงค์ต่อท้าย)
ตอนนี้ให้จุดที่ใช้งานอยู่เป็น(red,'d',3)
เพื่อให้มันชี้ไปยังสถานที่ที่อยู่เบื้องหลังf
ในdefg
ขอบ ตอนนี้ถือว่าเราทำปรับปรุงที่จำเป็นและตอนนี้ตามลิงค์ต่อท้ายเพื่อปรับปรุงจุดที่ใช้งานให้เป็นไปตามกฎ 3. (green,'d',3)
จุดที่ใช้งานใหม่คือ อย่างไรก็ตามd
-edge ที่ออกจากโหนดสีเขียวde
จึงมีเพียง 2 ตัวอักษร (blue,'f',1)
เพื่อที่จะหาจุดที่ใช้งานที่ถูกต้องที่เราเห็นได้ชัดว่าต้องทำตามขอบที่โหนดสีฟ้าและรีเซ็ต
ในกรณีที่ไม่ดีโดยเฉพาะอย่างยิ่งที่active_length
จะมีขนาดใหญ่เท่า
remainder
ซึ่งสามารถเป็นใหญ่เป็น n และอาจเป็นไปได้ว่าการหาจุดแอคทีฟที่ถูกต้องนั้นไม่เพียง แต่ต้องข้ามโหนดภายในหนึ่งโหนดเท่านั้น แต่อาจจะมากถึง n ในกรณีที่แย่ที่สุด นั่นหมายความว่าอัลกอริทึมนั้นมีความซับซ้อนO (n 2 ) ที่ซ่อนอยู่เพราะในแต่ละขั้นตอนremainder
มักจะเป็น O (n) และการปรับแต่งภายหลังให้กับโหนดที่แอ็คทีฟหลังจากติดตามลิงก์ต่อท้ายอาจเป็น O (n) เช่นกัน
ไม่เหตุผลคือถ้าหากเราต้องปรับจุดที่ใช้งาน (เช่นจากสีเขียวเป็นสีน้ำเงินดังกล่าวข้างต้น) นั่นจะนำเราไปสู่โหนดใหม่ที่มีลิงค์ต่อท้ายของตัวเองและactive_length
จะลดลง เมื่อเราติดตามลิงก์ของส่วนต่อท้ายที่เราสร้างส่วนแทรกที่เหลือactive_length
สามารถลดลงได้เท่านั้นและจำนวนการปรับค่าแอคทีฟจุดที่เราสามารถทำได้ในวิธีที่ไม่สามารถมีขนาดใหญ่กว่าactive_length
ในเวลาใดก็ตาม เนื่องจาก
active_length
ไม่สามารถมีขนาดใหญ่กว่าremainder
และremainder
เป็น O (n) ไม่เพียง แต่ในทุกขั้นตอนเท่านั้น แต่จำนวนรวมของการเพิ่มขึ้นที่ทำremainder
ตลอดกระบวนการทั้งหมดคือ O (n) เช่นกันจำนวนการปรับจุดที่ใช้งานอยู่คือ ล้อมรอบด้วย O (n)