แถวลำดับพลวัต

แถวลำดับพลวัต  หรืออาจเรียกว่า "แถวลำดับที่ขยายได้ (growable array)", "แถวลำดับที่เปลี่ยนขนาดได้ (resizable array)", "ตารางพลวัต (dynamic table)", "รายการแถวลำดับ (array list)" หรือ "เวกเตอร์ (vector)" เป็นรายการประเภทหนึ่ง มีคุณสมบัติการเข้าถึงโดยสุ่มเหมือนแถวลำดับ แต่ต่างจากแถวลำดับธรรมดาตรงที่สามารถขยายขนาดเองได้เมื่อใส่ข้อมูลเพิ่มเข้าไป
แถวลำดับพลวัต "ไม่ใช่" แถวลำดับจากการจองหน่วยความจำพลวัต เนื่องจาก "แถวลำดับจากการจองหน่วยความจำพลวัต" มีขนาดคงที่ ในขณะที่แถวลำดับพลวัตสามารถขยายขนาดได้ อย่างไรก็ตาม ในการอิมพลีเมนต์แถวลำดับพลวัต ก็อาจใช้แถวลำดับจากการจองหน่วยความจำพลวัตเป็นส่วนประกอบได้
การอิมพลีเมนต์แถวลำดับพลวัตมีหลากหลายแบบและได้มีการพัฒนาโดยเรื่อยมา (ดูเพิ่มเติมที่หัวข้อ "โครงสร้างข้อมูลอื่นที่คล้ายกัน" ข้างล่างนี้) ในบทความนี้จะนำเสนอการอิมพลีเมนต์รูปแบบหนึ่งที่เป็นที่นิยม มีประสิทธิภาพ ง่ายต่อการนำไปใช้งาน และง่ายต่อการวิเคราะห์
หลักการทำงาน.
แนวคิดเบื้องต้นของแถวลำดับพลวัตเริ่มต้นด้วยการจองหน่วยความจำพลวัตเพื่อสร้างแถวลำดับขนาดคงที่ขึ้นมาให้มีขนาดในระดับหนึ่ง เรียกขนาดนี้ว่าความจุของแถวลำดับพลวัต แถวลำดับที่สร้างมานี้จะประกอบไปด้วยสองส่วนคือ "ส่วนที่เก็บข้อมูล"ของแถวลำดับพลวัต กับ "ส่วนที่ไม่ได้ใช้งาน" ขนาดของส่วนที่เก็บข้อมูลนี้เรียกว่าขนาดของแถวลำดับพลวัต เมื่อมีการเพิ่มข้อมูลเข้ามาต่อท้ายก็เป็นเพียงการเปลี่ยนส่วนที่ไม่ได้ใช้งานให้มาเป็นส่วนเก็บข้อมูล ในขณะที่การลบข้อมูลออกด้านท้ายก็เป็นการเปลี่ยนส่วนเก็บข้อมูลให้มาเป็นส่วนที่ไม่ได้ใช้งาน ซึ่งการดำเนินการเปลี่ยนพื้นที่ไปมาระหว่าง "ส่วนที่เก็บข้อมูล" กับ "ส่วนที่ไม่ได้ใช้งาน" ใช้เวลาคงที่หรือ <math>\Theta (1) </math> เท่านั้น
ตราบใดที่"ขนาดของแถวลำดับพลวัต"มีค่าน้อยกว่าหรือเท่ากับ"ความจุของแถวลำดับพลวัต"ก็จะไม่มีปัญหาใด ๆ เกิดขึ้น แต่ถ้าหากเกิดเหตุการณ์ที่มีการเพิ่มข้อมูลจนทำให้"ขนาดของแถวลำดับพลวัต"มีค่าเกิน"ความจุของแถวลำดับพลวัต" สิ่งที่ต้องทำก็คือการขยายความจุ โดยการสร้างแถวลำดับขนาดคงที่อันใหม่ให้มีขนาดมากกว่าเดิม (มีความจุมากกว่าความจุเดิม) และคัดลอกข้อมูลจากแถวลำดับขนาดคงที่อันเดิมไปแถวลำดับขนาดคงที่อันใหม่ การดำเนินการขยายความจุนี้เสียเวลามาก กล่าวคือหากมีข้อมูลอยู่ <math>n</math> ตัวในแถวลำดับพลวัต การขยายความจุนี้จะใช้เวลาเชิงเส้น หรือ <math>\Theta (n) </math>
รูปแบบการทำงานของแถวลำดับพลวัต อาจแบ่งได้ออกเป็น 3 แบบ คือ
การทำงานที่ทราบจำนวนของข้อมูล.
ในกรณีที่ทราบว่าแถวลำดับพลวัตจะเก็บข้อมูลเท่าใด สามารถกำหนด"ความจุ"ให้เป็นค่าคงที่หนึ่งที่มีค่ามากกว่าหรือเท่ากับจำนวนของข้อมูล เพื่อรับประกันว่าจะไม่มีปัญหาที่"ขนาดของแถวลำดับพลวัต"มีค่าเกิน"ความจุของแถวลำดับพลวัต" ซึ่งนำไปสู่การเสียเวลาจากการขยายความจุ นอกจากนี้ เนื่องจากความจุเป็นค่าคงที่ พื้นที่ที่สูญเสียไปโดยไม่จำเป็นก็เป็นค่าคงที่เช่นเดียวกัน อย่างไรก็ตาม ในทางปฏิบัติมักจะไม่รู้ถึงจำนวนของข้อมูลที่แน่นอน และการเก็บข้อมูลด้วยแถวลำดับพลวัตที่ความจุเป็นค่าคงที่ในบางสถานการณ์ กลับทำให้สูญเสียพื้นที่โดยไม่จำเป็นอย่างมากมาย เช่นหากมีข้อมูล <math>n</math> ตัวที่จะนำมาเก็บบนรายการ <math>m</math> รายการตามคำสั่งที่กำหนด จะได้ว่ารายการหนึ่ง ๆ มีโอกาสที่จะมีข้อมูลมากสุดถึง <math>n</math> ตัว ดังนั้นหากใช้แถวลำดับพลวัตที่ความจุเป็นค่าคงที่ในการทำรายการทั้ง <math>m</math> รายการนั้น ก็ต้องหน่วยความจำถึง <math>\Theta (nm) </math> เพื่อรับประกันว่าข้อมูลจะสามารถเก็บได้หมด แต่ถ้าหากใช้แถวลำดับพลวัตที่ความจุไม่เป็นค่าคงที่ ("หัวข้อถัด ๆ ไป") จะใช้หน่วยความจำเพียง <math>\Theta (n + m) </math> เท่านั้น
การขยายความจุเป็นค่าคงที่.
เมื่อไม่ทราบจำนวนข้อมูลที่แน่นอน สิ่งที่อาจจะเกิดขึ้นได้ตลอดเวลาก็คือปัญหาที่"ขนาดของแถวลำดับพลวัต"มีค่าเกิน"ความจุของแถวลำดับพลวัต" ตามที่ได้กล่าวมาแล้วว่าวิธีการแก้ไขปัญหานี้ก็คือการ"ขยายความจุ" ในหัวข้อนี้ จะให้การขยายความจุเพิ่มความจุในแต่ละครั้งเป็นค่าคงที่ "c"
จากตาราง เมื่อมีการใส่ข้อมูลเข้ามาในแถวลำดับพลวัตเป็นจำนวน <math>n = k \times c</math> ตัว จะมีการทำงานจากการจองหน่วยความจำและคัดลอกข้อมูลถึง <math> (1 + 2 + ... + k) \times c = \Theta (k^2) </math> ครั้ง เนื่องจาก <math>k \propto n</math> จะได้ว่าการใส่ข้อมูลเข้าไป <math>n</math> ตัวใช้เวลาถึง <math>\Theta (n^2) </math> ถึงแม้วิธีนี้จะเสียเวลามาก แต่พื้นที่ที่สูญเสียไปก็คือส่วนที่ยังไม่ได้ใช้ซึ่งเป็นเพียงค่าคงที่เท่านั้น อย่างไรก็ตาม ในทางปฏิบัติมักจะคำนึงถึงความรวดเร็วของการทำงานมากกว่าพื้นที่ที่สูญเสียไป
การขยายความจุเป็นอัตราเรขาคณิตและการถัวเฉลี่ย.
เพื่อหลีกเลี่ยงการจองหน่วยความจำและคัดลอกข้อมูลหลายครั้งเกินความจำเป็น จึงมีแนวคิดใหม่ให้การขยายความจุเพิ่มความจุใหม่ในแต่ละครั้งเป็น "c" เท่า ซึ่งจะทำให้ความจุเพิ่มขึ้นเป็นอัตราเรขาคณิต รหัสเทียมแสดงการทำงานเป็นไปตามนี้
 function insertEnd ("dynarray" a, "element" e)
 if (a.size = a.capacity)
 // "resize a to c times of its current capacity:"
 a.capacity ← a.capacity * c
 // " (copy the contents to the new memory location here) "
 a[a.size] ← e
 a.size ← a.size + 1
เมื่อมีการใส่ข้อมูลตัวที่ <math>n</math> เข้ามาและเกิดการขยายเกิดขึ้น จะมีการจองหน่วยความจำและคัดลอกข้อมูลซึ่งใช้เวลา <math>\Theta (n) </math> ความจุจะเพิ่มขึ้น "c" เท่า นั่นหมายความว่าเมื่อพิจารณาถึงจำนวนคำสั่งที่ใช้รวมทั้งหมด ความถี่ในการขยายขนาดในแต่ละครั้งจะห่างขึ้นเรื่อย ๆ เป็นอัตราเรขาคณิต ทำให้เมื่อวิเคราะห์แบบถัวเฉลี่ย การเพิ่มข้อมูลที่บางครั้ง <math>\Theta (1) </math> บางครั้ง <math>\Theta (n) </math> จะสามารถถัวเฉลี่ยให้ทุก ๆ การดำเนินการกลายเป็น <math>\Theta (1) </math> ได้ ตารางข้างล่างนี้แสดงถึงการขยายความจุที่ความจุเพิ่มเป็นอัตราเรขาคณิต
สรุปได้ว่าเมื่อมีการใส่ข้อมูลเข้ามาในแถวลำดับพลวัตเป็นจำนวน <math>n = c^{k - 1}</math> ตัว จะมีการทำงาน <math>1 + c + c^2 + ... + c^{k - 1} = \Theta (c^k) </math> ครั้ง เนื่องจาก <math>k \propto \log_{c} n</math> จะได้ว่าการใส่ข้อมูลเข้าไป <math>n</math> ตัวใช้เวลา <math>\Theta (c^{\log_{c} n}) = \Theta (n) </math> ดังนั้นจึงอาจถัวเฉลี่ยให้การดำเนินการเพิ่มข้อมูลครั้งหนึ่งใช้เวลา <math>\Theta (1) </math> ได้ ข้อเสียของแถวลำดับพลวัตที่ขยายความจุเป็นอัตราเรขาคณิตคือพื้นที่ที่เสียไปโดยไม่จำเป็นอาจมีมากถึงเชิงเส้น <math>O (n) </math>
ค่า "c" นั้นสามารถเลือกใช้เป็นจำนวนใด ๆ ก็ได้ที่มากกว่า 1 เพื่อให้เห็นภาพได้ชัด หนังสือส่วนใหญ่ใช้ค่า "c" = 2 แต่เพื่อให้พื้นที่ที่เสียไปโดยไม่จำเป็นไม่มากนัก ในทางปฏิบัติมักจะใช้ค่า "c" ที่น้อยกว่านั้น เช่น <code>ArrayList</code> ของภาษาจาวาใช้ค่า "c" = 3/2 <code>list</code> ของภาษาไพธอน (ซึ่งเขียนด้วยภาษา C) ใช้ค่า "c" = 9/8
แถวลำดับพลวัตเป็นตัวอย่างที่นิยมใช้ในการสอนเรื่องการวิเคราะห์แบบถัวเฉลี่ย
การคืนหน่วยความจำให้กับระบบ.
เนื่องจากแถวลำดับพลวัตสามารถลบข้อมูลจากด้านปลายได้ อาจจะเป็นไปได้ที่หลังจากการลบข้อมูลหลาย ๆ ครั้งแล้ว จะมีพื้นที่ส่วนที่ไม่ได้ใช้งานเป็นจำนวนมากซึ่งทำให้ระบบเสียพื้นที่ไปโดยไม่จำเป็น วิธีการแก้ปัญหานี้สามารถทำได้โดยการคืนหน่วยความจำให้กับระบบโดยการลดความจุลง (โดยที่ความจุต้องมากกว่าขนาดของแถวลำดับพลวัตเพื่อให้ยังสามารถเก็บข้อมูลได้) การลดความจุมีวิธีดำเนินการคล้ายกับการขยายความจุ นั่นคือเป็นการสร้างแถวลำดับใหม่ขึ้นมา คัดลอกข้อมูลจากแถวลำดับเดิมไปยังแถวลำดับใหม่ จากนั้นก็คืนหน่วยความจำของแถวลำดับเดิมให้กับระบบ การดำเนินการลดความจุนี้จึงใช้เวลา <math>\Theta(n)</math> เช่นเดียวกับการขยายความจุ ดังนั้นหากดำเนินการลดความจุถี่เกินไป จะทำให้เมื่อถัวเฉลี่ยออกมาแล้วการดำเนินการแต่ละครั้งไม่เป็น <math>\Theta(1)</math>
เพื่อรักษาให้เวลาในการดำเนินการแต่ละครั้งเมื่อถัวเฉลี่ยแล้วได้ <math>\Theta(1)</math> ความจุที่ลดลงจึงควรเป็นอัตราเรขาคณิตเช่นเดียวกับการขยายความจุ กล่าวคือเมื่อขนาดของแถวลำดับพลวัตลดลงไปน้อยกว่า "cr" เท่าของความจุเดิม จึงจะมีการลดความจุ อย่างไรก็ตาม ข้อจำกัดของค่า "cr" คือ "cr" ต้องมีค่ามากกว่า "c"
สถานการณ์เลวร้ายสุดของการดำเนินการบนแถวลำดับพลวัตก็คือการที่ต้องมีการดำเนินการ"เพิ่มความจุ"หรือ"ลดความจุ"บ่อย ๆ ดังนั้นเมื่อพิจารณาในสถานการณ์นี้แล้ว สมมุติให้ขณะนี้มีข้อมูลอยู่ "n" ตัวซึ่ง "n" เป็นความจุของแถวลำดับพลวัตพอดีด้วย เมื่อใส่ข้อมูลเพิ่มอีกหนึ่งตัวจะเกิดจากขยายความจุ "c" เท่า มีความจุเป็น "nc" และมีขนาดเป็น "n" + 1 หากจะต้องการให้เกิดการลดความจุ จะต้องทำให้ขนาดของแถวลำดับพลวัตลดลงไปจนน้อยกว่า <math>{nc} / {c_r}</math> นั่นคือต้องลบข้อมูลออกไป <math>n - = { \times n}</math> และเพื่อที่เมื่อถัวเฉลี่ยกับการคัดลอกข้อมูลซึ่งใช้เวลา <math>\Theta(n)</math> แล้ว การดำเนินการแต่ละครั้งจะได้ใช้เวลา <math>\Theta(1)</math> จึงจะได้ว่าเวลาที่ใช้ในการลบข้อมูลจนเกิดการลดความจุต้องมากกว่าเวลาเชิงเส้น หรือ <math>\Omega(n)</math>
กรณีที่ <math>c_r < c</math> จะได้ว่าขนาดที่จะทำให้เกิดการคืนหน่วยความจำคือ <math>{nc} / {c_r}</math> ซึ่งมีค่ามากกว่า "n" เสียอีก ดังนั้นการกำหนด <math>c_r < c</math> จึงใช้ไม่ได้
กรณีที่ <math>c_r = c</math> จะได้ว่า <math>{ \times n} = 0</math> ซึ่งไม่เข้ากับเงื่อนไขที่ต้องการ หรือถ้าจะให้วิเคราะห์ จะได้ว่า ขนาดที่ทำให้เกิดการขยายความจุคือ "n" + 1 ส่วนขนาดที่ทำให้เกิดการลดความจุคือ "n" ดังนั้นเพียงเพิ่มและลบข้อมูลให้ขนาดเป็น "n" กับ "n" + 1 สลับกันไปเรื่อย ๆ ก็จะทำให้เวลารวมกลายเป็น <math>\Theta (n^2) </math> และถัวเฉลี่ยออกมาไม่ได้เวลาตามที่ต้องการ
กรณีที่ <math>c_r > c</math> จะได้ว่า <math>{ \times n} = \Omega(n)</math> ตามที่ต้องการ
ดังนั้น หากกำหนดให้ <math>c_r > c</math> จะได้ว่าสามารถถัวเฉลี่ยให้การดำเนินการแต่ละครั้งให้ใช้เวลาคงที่ หรือ <math>\Theta (1) </math> ได้ ส่วนหากกำหนดค่า "cr" เป็นอย่างอื่น อาจทำให้เกิดปัญหาหรือไม่มีประสิทธิภาพได้
ประสิทธิภาพเทียบกับโครงสร้างข้อมูลอื่น.
แถวลำดับพลวัตมีประสิทธิภาพเหมือนแถวลำดับทุกประการ นอกจากนี้ยังมีความสามารถมากขึ้นด้วยการสามารถเพิ่มหรือลบข้อมูลทางปลายได้เรื่อย ๆ ความสามารถของแถวลำดับพลวัตมีดังนี้
จะเห็นได้ว่าข้อดีทั้งหลายของแถวลำดับพลวัตมาจากข้อดีของแถวลำดับ เช่นการที่สามารถแคชข้อมูล มีความแน่นของข้อมูลมาก (ใช้เนื้อที่น้อย) การเข้าถึงโดยสุ่ม มีเพียงตัวแปรเก็บขนาดและความจุที่ต้องเก็บเพิ่มเท่านั้น ดังนั้นแถวลำดับพลวัตจึงเป็นโครงสร้างข้อมูลที่เหมาะในการทำงานหลาย ๆ อย่าง
เปรียบเทียบกับรายการโยงแล้ว แถวลำดับพลวัตสามารถเข้าถึงข้อมูลแบบสุ่มได้ ในขณะที่รายการโยงต้องใช้เวลาเชิงเส้น นอกจากนี้ยังมีความสามารถในการแคช ต่างจากรายการโยงที่ข้อมูลไม่ได้มีที่อยู่หน่วยความจำติดกัน ทำให้ไม่สามารถแคชข้อมูลได้ อย่างไรก็ตาม ความสามารถในการเพิ่มและลบข้อมูลกลางแถวลำดับพลวัตจะใช้เวลาเชิงเส้นเหมือนแถวลำดับเนื่องจากข้อมูลต้องเลื่อนเป็นจำนวนมาก ในขณะที่รายการโยงสามารถทำได้ในเวลาคงที่ ข้อด้อยนี้อาจใช้บัฟเฟอร์ช่องว่าง, "tiered vector" ช่วยได้ (อธิบายไว้ข้างล่างในหัวข้อ"โครงสร้างข้อมูลอื่นที่คล้ายกัน") สำหรับเครื่องที่มีหน่วยความจำน้อยหรือมีความกระจัดกระจายของพื้นที่หน่วยความจำสูง อาจจะไม่สามารถหาพื้นที่ติดกันที่มากพอในการจัดสรรให้แถวลำดับพลวัตได้
สำหรับต้นไม้สมดุล มีความสามารถในการดำเนินการต่าง ๆ ด้วยเวลาที่น้อยมาก เช่นเข้าถึงข้อมูลตัวใด ๆ, เพิ่มและลบข้อมูล ณ ตำแหน่งใด ๆ ในเวลา <math>O (\log n) </math> อย่างไรก็ตาม เนื้อที่ที่เก็บข้อมูลในโครงสร้างต้นไม้นั้นไม่ติดกัน จึงทำให้ไม่สามารถแคชได้
โครงสร้างข้อมูลอื่นที่คล้ายกัน.
บัฟเฟอร์ช่องว่าง  เป็นโครงสร้างข้อมูลที่คล้ายกับแถวลำดับพลวัต ความแตกต่างของบัฟเฟอร์ช่องว่างกับแถวลำดับพลวัตคือบัฟเฟอร์ช่องว่างจะมีพื้นที่ส่วนที่ไม่ได้ใช้แทรกอยู่เป็นจำนวนมาก แทนที่จะอยู่ฝั่งขวาเหมือนกับแถวลำดับพลวัต
แถวคอยสองหน้าก็สามารถทำให้มีความสามารถในการเข้าถึงโดยสุ่มได้ โดยใช้แนวคิดของแถวลำดับพลวัต ซึ่งพื้นที่ส่วนที่ไม่ได้ใช้งานจะมีทั้งด้านหน้าและที่ปลาย ดังนั้นเมื่อถัวเฉลี่ยแล้วจึงสามารถเพิ่มและลบข้อมูลทั้งด้านหน้าและด้านปลายได้ในเวลาคงที่ โดยที่มีความสามารถเข้าถึงข้อมูลแบบสุ่มด้วย
Goodrich ได้เสนอขั้นตอนวิธีในการสร้างแถวลำดับพลวัต เรียกว่า "Tiered Vector" ซึ่งใช้เวลา <math>O (\sqrt{n}) </math> ในการเพิ่มและลบข้อมูลกลางแถวลำดับพลวัต
ต้นไม้แถวลำดับแฮช  เป็นขั้นตอนวิธีของรายการแถบลำดับซึ่งตีพิมพ์ในปี 1996 ต้นไม้แถวลำดับแฮชใช้พื้นที่ <math>O (\sqrt{n}) </math> เมื่อ "n" คือจำนวนข้อมูลในแถวลำดับนี้ ขั้นตอนวิธีนี้ทำให้การเพิ่มอนุกรมเข้าไปที่ท้ายของต้นไม้แถวลำดับแฮช มีประสิทธิภาพทางเวลาถัวเฉลี่ย <math>O (1) </math>
ในปี 1999 Brodnik et al ได้นำเสนอแถวลำดับพลวัตซึ่งใช้พื้นที่เพียง <math>\sqrt{n}</math> ในทุก ๆ ขณะของการดำเนินการ เมื่อ "n" คือจำนวนข้อมูลในแถวลำดับในขณะนั้น นอกจากนี้ยังพิสูจน์ให้เห็นว่าขั้นตอนวิธีในการสร้างโครงสร้างข้อมูลแถวลำดับพลวัตใด ๆ ก็ต้องใช้พื้นที่อย่างน้อยเทียบเท่ากับแถวลำดับพลวัตของเขาหมด ยิ่งไปกว่านั้น การเพิ่มและลบข้อมูลจากปลายของแถวลำดับพลวัตนี้ยังใช้เวลาคงที่เท่านั้นโดยไม่ต้องมีการถัวเฉลี่ยเลย
ในปี 2002 Bagwell นำเสนอว่าสามารถใช้วีลิสต์ในการอิมพลีเมนต์แถวลำดับพลวัตได้
แถวลำดับพลวัตในภาษาต่าง ๆ.
การอิมพลีเมนต์แถวลำดับพลวัตในภาษาต่าง ๆ