ตั้งแต่วันแรกในชีวิตของเรา โปรแกรมเมอร์ เราทุกคนจัดการกับโครงสร้างข้อมูล: อาร์เรย์รายการที่เชื่อมโยงต้นไม้ชุดสแต็กและคิวเป็นคู่หูประจำวันของเราและ โปรแกรมเมอร์ที่มีประสบการณ์ รู้ว่าเมื่อใดและทำไมจึงควรใช้ ในบทความนี้เราจะดูว่าโครงสร้างข้อมูลที่ถูกละเลยบ่อยครั้งนั้นเป็นอย่างไร ไตร ส่องแสงในโดเมนแอปพลิเคชันที่มีคุณสมบัติเฉพาะเช่นเกมคำศัพท์
สำหรับผู้เริ่มต้นลองพิจารณาปริศนาคำศัพท์ง่ายๆ: ค้นหาคำที่ถูกต้องทั้งหมดในกระดานตัวอักษร 4x4 เชื่อมต่อตัวอักษรที่อยู่ติดกันในแนวนอนแนวตั้งหรือแนวทแยงมุม ตัวอย่างเช่นในกระดานต่อไปนี้เราจะเห็นตัวอักษร 'W', 'A', 'I' และ 'T' เชื่อมต่อกันเพื่อสร้างคำว่า 'WAIT'
วิธีแก้ปัญหาที่ไร้เดียงสาในการค้นหาคำที่ถูกต้องทั้งหมดคือการสำรวจกระดานโดยเริ่มจากมุมบนซ้ายจากนั้นเลื่อนระดับความลึกก่อนไปยังลำดับที่ยาวขึ้นโดยเริ่มจากตัวอักษรตัวที่สองในแถวแรกและอื่น ๆ ในบอร์ด 4x4 ที่อนุญาตให้เคลื่อนที่ในแนวตั้งแนวนอนและแนวทแยงมีลำดับ 12029640 ซึ่งมีความยาวตั้งแต่หนึ่งถึงสิบหกตัวอักษร
ตอนนี้เป้าหมายของเราคือการค้นหาโครงสร้างข้อมูลที่ดีที่สุดเพื่อใช้เครื่องมือตรวจสอบคำที่ถูกต้องนั่นคือคำศัพท์ของเรา บางประเด็นที่ควรทราบ:
เพื่ออธิบายประเด็นที่สองให้พิจารณากระดานต่อไปนี้: ไม่มีประเด็นในการสำรวจการเคลื่อนไหวที่ตามมาเนื่องจากไม่มีคำในพจนานุกรมที่ขึ้นต้นด้วย“ ASF”
microsoft windows เขียนด้วยภาษาอะไร
เราอยากให้โครงสร้างข้อมูลของเราตอบคำถามเหล่านี้โดยเร็วที่สุด ~ O (1) เวลาเข้าถึง (สำหรับการตรวจสอบลำดับ) จะเหมาะมาก!
เราสามารถกำหนดอินเทอร์เฟซคำศัพท์เช่นนี้ (ดู ที่นี่ สำหรับ GitHub repo):
public interface Vocabulary { boolean add(String word); boolean isPrefix(String prefix); boolean contains(String word); }
การใช้งาน contains()
วิธีการต้องการโครงสร้างข้อมูลสำรองที่ช่วยให้คุณค้นหาองค์ประกอบได้อย่างมีประสิทธิภาพในขณะที่ isPrefix()
วิธีการทำให้เราต้องหา“ องค์ประกอบที่ยิ่งใหญ่กว่าถัดไป” นั่นคือเราต้องจัดเรียงคำศัพท์ไม่ทางใดก็ทางหนึ่ง
เราสามารถแยกชุดที่ใช้แฮชออกจากรายชื่อผู้สมัครของเราได้อย่างง่ายดาย: ในขณะที่โครงสร้างดังกล่าวจะทำให้เราตรวจสอบ contains()
ได้ตลอดเวลา แต่มันจะทำงานได้ค่อนข้างแย่บน isPrefix()
ในกรณีที่เลวร้ายที่สุดที่ต้องใช้ เราสแกนทั้งชุด
ด้วยเหตุผลตรงกันข้ามเรายังสามารถยกเว้นรายการที่เชื่อมโยงที่เรียงลำดับได้เนื่องจากต้องมีการสแกนรายการอย่างน้อยที่สุดจนถึงองค์ประกอบแรกที่มากกว่าหรือเท่ากับคำที่ค้นหาหรือคำนำหน้า
สองตัวเลือกที่ถูกต้องคือการใช้รายการสำรองอาร์เรย์ที่เรียงลำดับหรือต้นไม้ไบนารี
ในรายการสำรองอาร์เรย์ที่เรียงลำดับเราสามารถใช้การค้นหาแบบไบนารีเพื่อค้นหาลำดับปัจจุบันหากมีอยู่หรือองค์ประกอบที่ใหญ่กว่าถัดไปโดยเสียค่าใช้จ่าย O (log2 (n)) , ที่ไหน n คือจำนวนคำในพจนานุกรม
เราสามารถใช้คำศัพท์ที่ได้รับการสนับสนุนจากอาร์เรย์ซึ่งจะรักษาการเรียงลำดับของสิ่งที่ชอบอยู่เสมอ นี้ โดยใช้มาตรฐาน java.util.ArrayList
และ java.util.Collections.binarySeach
:
public class ListVocabulary implements Vocabulary { private List words = new ArrayList(); /** * Constructor that adds all the words and then sorts the underlying list */ public ListVocabulary(Collection words) { this.words.addAll(words); Collections.sort(this.words); } public boolean add(String word) { int pos = Collections.binarySearch(words, word); // pos > 0 means the word is already in the list. Insert only // if it's not there yet if (pos = 0) { // The prefix is a word. Check the following word, because we are looking // for words that are longer than the prefix if (pos +1 = 0; } }
หากเราตัดสินใจที่จะใช้ต้นไม้ไบนารีการใช้งานอาจสั้นลงและสวยงามมากขึ้น (อีกครั้งนี่คือไฟล์ เชื่อมโยงไปยังรหัส ):
public class TreeVocabulary extends TreeSet implements Vocabulary { public TreeVocabulary(Collection c) { super(c); } public boolean isPrefix(String prefix) { String nextWord = ceiling(prefix); if (nextWord == null) { return false; } if (nextWord.equals(prefix)) { Set tail = tailSet(nextWord, false); if (tail.isEmpty()) { return false; } nextWord = tail.iterator().next(); } return nextWord.startsWith(prefix); } /** * There is a mismatch between the parameter types of vocabulary and TreeSet, so * force call to the upper-class method */ public boolean contains(String word) { return super.contains(word); } }
ในทั้งสองกรณีเราสามารถคาดหวังได้ O (บันทึก n) ประสิทธิภาพสำหรับแต่ละวิธีการเข้าถึง (contains()
และ isPrefix()
) สำหรับความต้องการพื้นที่จำเป็นต้องใช้ทั้งการใช้งานอาร์เรย์ที่สำรองไว้และการใช้งานที่มีต้นไม้สำรอง O (n + M) โดยที่ n คือจำนวนคำในพจนานุกรมและ M คือขนาดไบต์ของพจนานุกรมนั่นคือผลรวมของความยาวของสตริงในพจนานุกรม
ประสิทธิภาพของลอการิทึมและหน่วยความจำเชิงเส้นก็ไม่เลว แต่ยังมีคุณสมบัติอีกสองสามประการของโดเมนแอปพลิเคชันของเราที่สามารถนำเราไปสู่ประสิทธิภาพที่ดีขึ้น:
นี่คือจุดที่ trie (ออกเสียงว่า 'ลอง') เข้ามา แต่อะไรกันแน่ คือ สามเส้า? ความพยายามเป็นโครงสร้างข้อมูลที่ถูกละเลยซึ่งพบได้ในหนังสือ แต่ไม่ค่อยพบในไลบรารีมาตรฐาน
สำหรับแรงจูงใจก่อนอื่นให้พิจารณาเด็กโปสเตอร์ของ Computer Science นั่นคือต้นไม้ไบนารี ตอนนี้เมื่อเราวิเคราะห์ประสิทธิภาพของต้นไม้ไบนารีและพูดว่าการดำเนินการ x คือ O (บันทึก (n)) เรากำลังพูดถึง log base 2 อยู่ตลอดเวลา แต่ถ้าเป็น binary tree เราใช้ ternary tree แทนซึ่งทุกโหนดจะมีลูกสามลูก (หรือ fan-out ใน 3 ตัว) จากนั้นเราจะพูดถึง log base 3 (นั่นคือการปรับปรุงประสิทธิภาพแม้ว่าจะเป็นเพียงปัจจัยคงที่เท่านั้น) โดยพื้นฐานแล้วต้นไม้ของเราจะกว้างขึ้น แต่สั้นลงและเราสามารถทำการค้นหาได้น้อยลงเนื่องจากเราไม่จำเป็นต้องลดระดับลงมากนัก ลึกมาก.
ก้าวไปอีกขั้นจะเกิดอะไรขึ้นถ้าเรามีต้นไม้ที่มี fan-out เท่ากับจำนวนค่าที่เป็นไปได้ของประเภทข้อมูลของเรา?
นี่คือแรงจูงใจเบื้องหลัง Trie และอย่างที่คุณอาจเดาได้ว่า Trie เป็นต้นไม้จริง ๆ แล้วก็คือต้นไม้ Trie นั่นเอง!
แต่ตรงกันข้ามกับไบนารีทรีส่วนใหญ่ที่คุณจะใช้ในการจัดเรียงสตริงซึ่งจะเก็บคำทั้งหมดไว้ในโหนดของพวกเขาแต่ละโหนดของไตรมีอักขระเพียงตัวเดียว (ไม่ใช่อย่างนั้นอย่างที่เราจะเห็นในไม่ช้า) และ มีพัดลมออกสูงสุดเท่ากับความยาวของตัวอักษร ในกรณีของเราความยาวของตัวอักษรคือ 26 ดังนั้นโหนดของ trie จึงมีพัดลมออกสูงสุด 26 และในขณะที่ต้นไม้ไบนารีที่สมดุลมี log2 (n) ความลึกความลึกสูงสุดของสามเท่ากับความยาวสูงสุดของคำ! (อีกครั้งกว้างกว่า แต่สั้นกว่า)
อัตรารายชั่วโมงของผู้รับเหมาเทียบกับเงินเดือน
ภายใน Trie คำที่มีก้านเดียวกัน (คำนำหน้า) จะแบ่งพื้นที่หน่วยความจำที่ตรงกับก้าน
จุดพักความละเอียดการออกแบบเว็บที่ตอบสนอง
เพื่อให้เห็นภาพความแตกต่างลองพิจารณาพจนานุกรมขนาดเล็กที่ประกอบด้วยคำห้าคำ สมมติว่าตัวอักษรภาษากรีกระบุตัวชี้และสังเกตว่าในสาม อักขระสีแดงหมายถึงโหนดที่มีคำที่ถูกต้อง .
ดังที่เราทราบในแผนภูมิตัวชี้ไปยังองค์ประกอบลูกโดยปกติจะใช้ตัวแปรซ้ายและขวาเนื่องจากค่าพัดลมออกสูงสุดถูกกำหนดไว้ที่สองตัว
ในการจัดทำดัชนีสามตัวอักษร 26 ตัวแต่ละโหนดมีลูกที่เป็นไปได้ 26 ลูกดังนั้นจึงมี 26 ตัวชี้ที่เป็นไปได้ แต่ละโหนดมีอาร์เรย์ของต้นไม้ย่อย 26 (พอยน์เตอร์ถึง) ซึ่งแต่ละค่าอาจเป็นโมฆะ (ถ้าไม่มีลูกนั้น) หรือโหนดอื่น
ถ้าอย่างนั้นเราจะค้นหาคำใน Trie ได้อย่างไร? นี่คือวิธีการที่กำหนด String s
จะระบุโหนดที่ตรงกับตัวอักษรสุดท้ายของคำหากมีอยู่ในแผนภูมิ:
public LowercaseTrieVocabulary getNode(String s) { LowercaseTrieVocabulary node = this; for (int i = 0; i LOWERCASE.getIndex(s.charAt(i))
เพียงแค่คืนตำแหน่งของไฟล์ ith อักขระในตัวอักษร บนโหนดที่ส่งคืนคุณสมบัติบูลีน node
บ่งชี้ว่าโหนดตรงกับตัวอักษรสุดท้ายของคำนั่นคือตัวอักษรที่ทำเครื่องหมายด้วยสีแดงในตัวอย่างก่อนหน้านี้ เนื่องจากแต่ละโหนดมีตัวนับจำนวนลูกถ้าตัวนับนี้เป็นค่าบวกจะมีคำที่ยาวกว่าในพจนานุกรมที่มีสตริงปัจจุบันเป็นคำนำหน้า หมายเหตุ: โหนดไม่จำเป็นต้องอ้างอิงถึงอักขระที่สอดคล้องกันเนื่องจากมีนัยอยู่ในตำแหน่งในสาม
การวิเคราะห์ประสิทธิภาพ
สิ่งที่ทำให้โครงสร้าง trie ทำงานได้ดีในสถานการณ์เหล่านี้คือค่าใช้จ่ายในการค้นหาคำหรือคำนำหน้าจะคงที่และขึ้นอยู่กับจำนวนอักขระในคำเท่านั้นและไม่ขึ้นอยู่กับขนาดของคำศัพท์
ในโดเมนเฉพาะของเราเนื่องจากเรามีสตริงที่มีอักขระไม่เกิน 16 ตัวจึงจำเป็นต้องมี 16 ขั้นตอนในการค้นหาคำที่อยู่ในคำศัพท์ในขณะที่คำตอบเชิงลบใด ๆ เช่นคำหรือคำนำหน้าไม่ได้อยู่ในไตร มากที่สุด 16 ขั้นตอนเช่นกัน! เมื่อพิจารณาว่าก่อนหน้านี้เราได้เพิกเฉยต่อความยาวของสตริงเมื่อคำนวณความซับซ้อนของเวลาทำงานสำหรับทั้งรายการที่เรียงลำดับอาร์เรย์สำรองและแผนภูมิซึ่งซ่อนอยู่ในการเปรียบเทียบสตริงเราสามารถเพิกเฉยได้ที่นี่และระบุอย่างปลอดภัยว่าการค้นหาเสร็จสิ้น ใน O (1) เวลา.
พิจารณาความต้องการพื้นที่ (และจำไว้ว่าเราได้ระบุด้วย ม ไบต์ขนาดของพจนานุกรม) สามมิติสามารถมีได้ ม โหนดในกรณีที่เลวร้ายที่สุดหากไม่มีสองสตริงที่ใช้คำนำหน้าร่วมกัน แต่เนื่องจากเราสังเกตเห็นว่าในพจนานุกรมมีความซ้ำซ้อนสูงจึงมีการบีบอัดข้อมูลจำนวนมาก พจนานุกรมภาษาอังกฤษที่ใช้ในโค้ดตัวอย่างคือ 935,017 ไบต์และต้องการ 250,264 โหนดโดยมีอัตราส่วนการบีบอัดประมาณ 73%
อย่างไรก็ตามแม้จะเป็นเช่นนี้แม้แต่ Trie ที่บีบอัดก็มักต้องการหน่วยความจำมากกว่าต้นไม้หรืออาร์เรย์ เนื่องจากแต่ละโหนดอย่างน้อย 26 x sizeof(pointer)
ไบต์เป็นสิ่งที่จำเป็นบวกค่าโสหุ้ยสำหรับอ็อบเจ็กต์และแอ็ตทริบิวต์เพิ่มเติม บนเครื่อง 64 บิตแต่ละโหนดต้องการมากกว่า 200 ไบต์ในขณะที่อักขระสตริงต้องการไบต์เดียวหรือสองตัวหากเราพิจารณาสตริง UTF
ที่เกี่ยวข้อง: ข้อผิดพลาดทั่วไป 10 อันดับแรกที่นักพัฒนา Java ทำ: บทช่วยสอนสำหรับผู้เริ่มต้นใช้งาน Java การทดลองและการทดสอบประสิทธิภาพ
แล้วประสิทธิภาพล่ะ? การใช้คำศัพท์ได้รับการทดสอบในสองสถานการณ์ที่แตกต่างกัน: ตรวจสอบสตริงสุ่ม 20,000,000 สตริงและค้นหาคำทั้งหมดใน 15,000 กระดานที่สร้างแบบสุ่มจากรายการคำเดียวกัน
โครงสร้างข้อมูลสี่แบบได้รับการวิเคราะห์: รายการที่เรียงลำดับอาร์เรย์สำรองต้นไม้ไบนารีทรีที่อธิบายไว้ข้างต้นและสามส่วนโดยใช้อาร์เรย์ของไบต์ที่สอดคล้องกับดัชนีตัวอักษรของอักขระ (การเพิ่มประสิทธิภาพเล็กน้อยและใช้งานได้ง่าย) นี่คือผลลัพธ์ในหน่วยมิลลิวินาที:

จำนวนการเคลื่อนไหวโดยเฉลี่ยในการแก้ปัญหากระดานคือ 2,188 สำหรับการย้ายแต่ละครั้งจะมีการค้นหาคำและการค้นหาคำนำหน้าเช่นสำหรับการตรวจสอบกระดานทั้งหมดมีการค้นหาคำมากกว่า 32 ล้านคำและการค้นหาคำนำหน้า 32M หมายเหตุ: สิ่งเหล่านี้สามารถทำได้ในขั้นตอนเดียวฉันแยกมันออกจากกันเพื่อความชัดเจนในการจัดแสดง การกระชับเข้าด้วยกันในขั้นตอนเดียวจะช่วยลดเวลาในการแก้ปัญหาของบอร์ดได้เกือบครึ่งและอาจจะชอบ Trie มากยิ่งขึ้น
ดังที่เห็นได้ข้างต้นการค้นหาคำจะทำงานได้ดีขึ้นกับ trie แม้ว่าจะใช้สตริงและยังเร็วกว่าเมื่อใช้ดัชนีตัวอักษรโดยที่การค้นหาแบบหลังจะเร็วกว่าต้นไม้ไบนารีมาตรฐานถึงสองเท่า ความแตกต่างในการแก้บอร์ดนั้นชัดเจนยิ่งขึ้นด้วยโซลูชันไตร - อักษร - ดัชนีที่เร็วกว่ารายการและแผนภูมิมากกว่าสี่เท่า
ห่อ
Trie เป็นโครงสร้างข้อมูลเฉพาะทางที่ต้องการหน่วยความจำมากกว่าต้นไม้และรายการ อย่างไรก็ตามเมื่อใช้ลักษณะเฉพาะของโดเมนเช่นตัวอักษรที่ จำกัด และมีความซ้ำซ้อนสูงในส่วนแรกของสตริงจะมีประสิทธิภาพมากในการจัดการกับการเพิ่มประสิทธิภาพการทำงาน
อ้างอิง
คำอธิบายอย่างละเอียดเกี่ยวกับการพยายามและตัวอักษรมีอยู่ในบทที่ 5 ของหนังสือ 'Algorithms, 4th edition' ของ Robert Sedgewick เว็บไซต์คู่หูที่ Princeton มีไฟล์ รหัสสำหรับการติดตั้ง ของ Alphabet และ TrieST ที่กว้างขวางกว่าตัวอย่างของฉัน
javascript typeerror ไม่ใช่ฟังก์ชัน
นอกจากนี้ยังสามารถดูคำอธิบายของ Trie และการใช้งานสำหรับภาษาต่างๆได้ใน Wikipedia และคุณสามารถดูสิ่งนี้ ทรัพยากรสามมหาวิทยาลัยบอสตัน เช่นกัน.
ที่เกี่ยวข้อง: เข็มในกองหญ้า: การสอนอัลกอริทึมการค้นหาข้อความขนาดใหญ่ที่ดี ทำความเข้าใจพื้นฐาน
Trie คืออะไร?
Trie คือโครงสร้างข้อมูลตามลำดับซึ่งเป็นโครงสร้างการค้นหาชนิดหนึ่งที่ใช้ในการจัดเก็บโครงสร้างข้อมูลที่เชื่อมโยงกัน เรียกอีกอย่างว่าต้นไม้ Radix หรือต้นไม้คำนำหน้า
Trie บีบอัดคืออะไร?
Trie ที่บีบอัดเป็นโครงสร้างข้อมูล Trie โดยมีกฎเพิ่มเติมว่าทุกโหนดต้องมีลูกสองคนขึ้นไป
การออกเสียง trie ใดถูกต้อง?
trie ออกเสียงว่า 'try' แม้ว่าชื่อ trie จะมาจาก 'retrieval'
โครงสร้างข้อมูล Trie เหมาะสำหรับอะไร?
โครงสร้างข้อมูล trie เหมาะอย่างยิ่งกับอัลกอริทึมที่ตรงกันเนื่องจากเป็นไปตามคำนำหน้าของแต่ละสตริง โดยทั่วไปจะใช้การพยายามเมื่อจัดการกับกลุ่มของสตริงแทนที่จะเป็นสตริงเดี่ยวทำให้สามารถแก้ปัญหาได้หลากหลาย