มีวิธีปฏิบัติสำหรับโครงสร้างโหนดที่เชื่อมโยงที่จะไม่เปลี่ยนรูป?


26

ฉันตัดสินใจเขียนรายการที่ลิงก์เดี่ยว ๆ และมีแผนที่จะทำให้โครงสร้างโหนดเชื่อมโยงภายในไม่เปลี่ยนรูป

ฉันวิ่งเข้าไปในอุปสรรค์ ว่าฉันมีโหนดเชื่อมโยงต่อไปนี้ (จากaddการดำเนินการก่อนหน้า):

1 -> 2 -> 3 -> 4

5และบอกว่าผมต้องการที่จะผนวก

การทำเช่นนี้ตั้งแต่โหนด4จะไม่เปลี่ยนรูปฉันต้องสร้างสำเนาใหม่ของ4แต่แทนที่ของสนามใหม่ที่มีโหนดnext 5ปัญหาคือตอนนี้3กำลังอ้างอิงเก่า4; 5หนึ่งโดยไม่ต้องต่อท้าย ตอนนี้ฉันต้องการคัดลอก3และแทนที่nextฟิลด์เพื่ออ้างอิง4สำเนา แต่ตอนนี้2กำลังอ้างอิงเก่า3...

หรือกล่าวอีกนัยหนึ่งเมื่อต้องการต่อท้ายรายการทั้งหมดจะต้องคัดลอก

คำถามของฉัน:

  • ความคิดของฉันถูกต้องหรือไม่ มีวิธีการผนวกโดยไม่คัดลอกโครงสร้างทั้งหมดหรือไม่

  • เห็นได้ชัดว่า "จาวาที่มีประสิทธิภาพ" มีคำแนะนำ:

    คลาสควรจะไม่เปลี่ยนรูปเว้นแต่จะมีเหตุผลที่ดีมากที่ทำให้พวกเขาเปลี่ยนแปลงได้ ...

    นี่เป็นกรณีที่ดีสำหรับการเปลี่ยนแปลงหรือไม่?

ฉันไม่คิดว่านี่เป็นคำตอบที่ซ้ำกันเนื่องจากฉันไม่ได้พูดถึงรายการ เห็นได้ชัดว่าจะต้องมีการเปลี่ยนแปลงเพื่อให้สอดคล้องกับอินเทอร์เฟซ (โดยไม่ต้องทำอะไรบางอย่างเช่นการรักษารายการใหม่ภายในและดึงมันออกมาผ่านทะเยอทะยานในความคิดที่สองแม้ว่าแม้จะต้องมีการกลายพันธุ์; ฉันกำลังพูดถึงว่า internals ของรายการจะต้องไม่เปลี่ยนรูป


ฉันจะบอกว่าใช่ - คอลเลกชันที่ไม่เปลี่ยนรูปแบบที่หายากใน Java เป็นทั้งที่มีวิธีการกลายพันธุ์แทนที่ที่จะโยนหรือCopyOnWritexxxชั้นเรียนที่มีราคาแพงอย่างไม่น่าเชื่อที่ใช้สำหรับการมัลติเธรด ไม่มีใครคาดว่าคอลเลกชันที่จะไม่เปลี่ยนรูปจริงๆ (แม้ว่ามันจะสร้างนิสัยใจคอบาง)
Ordous

9
(ความเห็นเกี่ยวกับคำพูด) นั่นเป็นคำพูดที่ดีมักจะเข้าใจผิดอย่างมาก การเปลี่ยนไม่ได้ควรนำไปใช้กับValueObjectเนื่องจากประโยชน์ของมันแต่ถ้าคุณกำลังพัฒนาใน Java มันไม่สามารถใช้การเปลี่ยนแปลงในสถานที่ที่คาดว่าจะเกิดความไม่แน่นอน เวลาส่วนใหญ่ชั้นเรียนมีลักษณะบางอย่างที่ควรจะไม่เปลี่ยนรูปแบบและบางแง่มุมที่จะต้องไม่แน่นอนขึ้นอยู่กับความต้องการ
rwong

2
ที่เกี่ยวข้อง (อาจซ้ำกัน): การรวบรวมวัตถุดีกว่าไม่เปลี่ยนรูปหรือไม่ "หากไม่มีค่าโสหุ้ยในการทำงานคอลเลกชันนี้ (คลาส LinkedList) จะถูกนำมาใช้เป็นคอลเลกชันที่ไม่เปลี่ยนรูปแบบได้หรือไม่"
ริ้น

ทำไมคุณถึงสร้างรายการเชื่อมโยงที่เปลี่ยนแปลงไม่ได้? ประโยชน์ที่ใหญ่ที่สุดของรายการที่เชื่อมโยงคือความไม่แน่นอนคุณจะไม่ดีกว่ากับอาเรย์
Pieter B

3
คุณช่วยฉันเข้าใจความต้องการของคุณได้ไหม? คุณต้องการที่จะมีรายการที่ไม่เปลี่ยนรูปแบบที่คุณต้องการที่จะกลายพันธุ์? นั่นไม่ใช่ความขัดแย้งใช่มั้ย คุณหมายถึงอะไรกับ "ภายใน" ของรายการ? คุณหมายความว่าโหนดที่เพิ่มก่อนหน้านี้ไม่ควรแก้ไขได้ในภายหลัง แต่อนุญาตให้ผนวกโหนดสุดท้ายในรายการเท่านั้น
null

คำตอบ:


46

ด้วยรายการในภาษาที่ใช้งานได้คุณเกือบจะทำงานกับส่วนหัวและส่วนท้ายองค์ประกอบแรกและส่วนที่เหลือของรายการ การเตรียมเป็นเรื่องธรรมดามากเพราะในขณะที่คุณคาดการณ์ไว้การต่อท้ายต้องคัดลอกรายการทั้งหมด (หรือโครงสร้างข้อมูลอื่น ๆ ที่ขี้เกียจที่ไม่คล้ายกับรายการที่เชื่อมโยงอย่างแม่นยำ)

ในภาษาที่มีความจำเป็นการต่อท้ายนั้นเป็นเรื่องธรรมดามากเพราะมันมีแนวโน้มที่จะรู้สึกเป็นธรรมชาติมากขึ้นและคุณไม่สนใจเกี่ยวกับการลบล้างการอ้างอิงไปยังรายการเวอร์ชั่นก่อนหน้า

ตัวอย่างของสาเหตุที่การสรุปไม่ต้องการคัดลอกรายการทั้งหมดให้พิจารณาว่าคุณมี:

2 -> 3 -> 4

เตรียม a 1ช่วยให้คุณ:

1 -> 2 -> 3 -> 4

แต่โปรดทราบว่าไม่สำคัญว่าคนอื่นยังคงมีการอ้างอิงถึง2ในฐานะหัวหน้ารายการของพวกเขาเพราะรายการนั้นไม่เปลี่ยนรูปและการเชื่อมโยงจะไปทางเดียว ไม่มีทางที่จะบอกไม่ได้แม้จะมีถ้าคุณมีเพียงการอ้างอิงถึง1 2ตอนนี้ถ้าคุณต่อท้าย5รายการใดรายการหนึ่งคุณจะต้องทำสำเนาของรายการทั้งหมดเพราะไม่เช่นนั้นจะปรากฏในรายการอื่นเช่นกัน


อ่าจุดดีที่ว่าทำไมการจัดเตรียมไม่ต้องการสำเนา ฉันไม่คิดอย่างนั้น
Carcigenicate

17

คุณถูกต้องผนวกจะต้องคัดลอกรายการทั้งหมดหากคุณไม่ต้องการที่จะกลายพันธุ์โหนดใด ๆ ในสถานที่ สำหรับเราจำเป็นต้องตั้งค่าnextตัวชี้ของโหนดสองถึงสุดท้ายซึ่งในการตั้งค่าไม่เปลี่ยนรูปสร้างโหนดใหม่แล้วเราต้องตั้งค่าnextตัวชี้ของโหนดที่สามถึงสุดท้ายและอื่น ๆ

ฉันคิดว่าปัญหาหลักที่นี่ไม่เปลี่ยนไม่ได้และการappendดำเนินการไม่ดี ทั้งสองอย่างดีในโดเมนของตน การผสมเข้าด้วยกันไม่ดี: อินเทอร์เฟซธรรมชาติ (ประสิทธิภาพ) สำหรับรายการที่ไม่เปลี่ยนแปลงเน้นการจัดการที่ด้านหน้าของรายการ แต่สำหรับรายการที่ไม่แน่นอนมันมักจะเป็นธรรมชาติมากขึ้นในการสร้างรายการโดยต่อท้ายรายการตั้งแต่แรกจนถึงสุดท้าย

ดังนั้นฉันขอแนะนำให้คุณตัดสินใจ: คุณต้องการอินเทอร์เฟซแบบชั่วคราวหรือแบบถาวรหรือไม่ การดำเนินการส่วนใหญ่สร้างรายการใหม่และปล่อยให้เวอร์ชันที่ไม่ได้แก้ไขสามารถเข้าถึงได้ (ถาวร) หรือคุณเขียนโค้ดเช่นนี้ (ชั่วคราว):

list.append(x);
list.prepend(y);

ตัวเลือกทั้งสองนั้นใช้ได้ แต่การติดตั้งควรสะท้อนถึงส่วนต่อประสาน: โครงสร้างข้อมูลถาวรได้รับประโยชน์จากโหนดที่ไม่เปลี่ยนรูปในขณะที่หนึ่ง ephemeral ต้องการความไม่แน่นอนภายในเพื่อตอบสนองประสิทธิภาพที่สัญญาไว้โดยปริยาย java.util.Listและอินเทอร์เฟซอื่น ๆ เป็นชั่วคราว: การใช้พวกเขาในรายการไม่เปลี่ยนรูปไม่เหมาะสมและในความเป็นจริงประสิทธิภาพการทำงานที่อันตราย อัลกอริธึมที่ดีเกี่ยวกับโครงสร้างข้อมูลที่ไม่แน่นอนมักจะแตกต่างจากอัลกอริธึมที่ดีในโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปดังนั้นการรวบรวมโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปแบบให้กลายเป็นสิ่งที่ไม่แน่นอน (หรือในทางกลับกัน)

ในขณะที่รายการถาวรมีข้อเสียบางอย่าง (ไม่ท้ายที่มีประสิทธิภาพ) นี้ไม่จำเป็นต้องเป็นปัญหาร้ายแรงเมื่อโปรแกรมการทำงาน: ขั้นตอนวิธีการหลายคนได้สูตรได้อย่างมีประสิทธิภาพโดยการขยับความคิดและการใช้ฟังก์ชั่นขั้นสูงเช่นmapหรือfold(เพื่อชื่อสองคนค่อนข้างดั้งเดิม ) หรือโดยprependingซ้ำแล้วซ้ำอีก นอกจากนี้ไม่มีใครบังคับให้คุณใช้เฉพาะโครงสร้างข้อมูลนี้: เมื่อผู้อื่น (ชั่วคราวหรือถาวร แต่ซับซ้อนกว่า) ใช้ให้เหมาะสม ฉันควรทราบด้วยว่ารายการแบบถาวรมีข้อดีบางประการสำหรับเวิร์กโหลดอื่น ๆ : พวกเขาแบ่งปันก้อยซึ่งสามารถประหยัดหน่วยความจำได้


4

หากคุณมีรายการที่เชื่อมโยงเดี่ยวคุณจะทำงานกับด้านหน้าถ้ามันมากกว่าที่คุณจะกลับมา

ภาษาที่ใช้งานได้เช่น prolog และ haskel ให้วิธีง่าย ๆ ในการรับส่วนหน้าและส่วนที่เหลือของอาร์เรย์ การต่อท้ายด้านหลังเป็นการดำเนินการ O (n) โดยคัดลอกแต่ละโหนด


1
ฉันเคยใช้ Haskell Afaik มันยังหลีกเลี่ยงปัญหาบางส่วนโดยใช้ความเกียจคร้าน ฉันกำลังผนวกเนื่องจากฉันคิดว่านั่นเป็นสิ่งที่คาดหวังจากListอินเทอร์เฟซ (ฉันอาจจะผิดที่นั่น) ฉันไม่คิดว่าบิตตอบคำถามจริงๆทั้ง รายการทั้งหมดจะยังคงต้องคัดลอก; มันจะทำให้เข้าถึงองค์ประกอบสุดท้ายที่เพิ่มเร็วขึ้นเนื่องจากไม่จำเป็นต้องมีการแวะผ่าน
Carcigenicate

การสร้างคลาสตามอินเทอร์เฟซที่ไม่ตรงกับโครงสร้างข้อมูล / อัลกอริทึมพื้นฐานเป็นการเชื้อเชิญความเจ็บปวดและความไร้ประสิทธิภาพ
วงล้อประหลาด

JavaDocs ใช้คำว่า "ผนวก" อย่างชัดเจน คุณกำลังบอกว่าดีกว่าที่จะเพิกเฉยต่อการดำเนินการนี้
Carcigenicate

2
@Carcigenicate ไม่ฉันกำลังบอกว่ามันเป็นความผิดพลาดที่จะลองใส่รายการที่เชื่อมโยงเดี่ยว ๆ กับโหนดที่ไม่เปลี่ยนรูปให้กลายเป็นรูปแบบของjava.util.List
ratchet freak

1
ด้วยความเกียจคร้านจะไม่มีรายการเชื่อมโยงอีกต่อไป ไม่มีรายการจึงไม่มีการกลายพันธุ์ (ผนวก) มีรหัสบางอย่างที่สร้างรายการถัดไปเมื่อมีการร้องขอ แต่ไม่สามารถใช้ได้ทุกที่ที่มีการใช้รายการ กล่าวคือถ้าคุณเขียนวิธีใน Java ที่คาดว่าจะกลายพันธุ์รายการที่ส่งผ่านเป็นอาร์กิวเมนต์รายการนั้นจะต้องไม่แน่นอนในสถานที่แรก วิธีการสร้างต้องใช้การปรับโครงสร้างที่สมบูรณ์ (การปรับโครงสร้างองค์กร) ของรหัสเพื่อให้มันทำงาน - และเพื่อให้เป็นไปได้ที่จะทำไปกับรายการทางกายภาพ
rwong

4

ดังที่คนอื่น ๆ ได้ชี้ให้เห็นว่าคุณถูกต้องว่ารายการที่เชื่อมโยงเดี่ยวที่ไม่เปลี่ยนรูปจะต้องคัดลอกรายการทั้งหมดเมื่อดำเนินการต่อท้าย

บ่อยครั้งที่คุณสามารถใช้วิธีการแก้ปัญหาของการใช้อัลกอริทึมของคุณในแง่ของconsการดำเนินการ (เสริม) และจากนั้นกลับรายการสุดท้ายเพียงครั้งเดียว สิ่งนี้ยังคงต้องคัดลอกรายการหนึ่งครั้ง แต่ค่าใช้จ่ายด้านความซับซ้อนนั้นเป็นเส้นตรงตามความยาวของรายการในขณะที่การใช้ผนวกข้อมูลซ้ำ ๆ คุณสามารถรับความซับซ้อนกำลังสองได้อย่างง่ายดาย

รายการความแตกต่าง (ดูตัวอย่างที่นี่ ) เป็นทางเลือกที่น่าสนใจ รายการผลต่างล้อมรายการและให้การดำเนินการผนวกในเวลาคงที่ คุณทำงานกับ wrapper โดยทั่วไปตราบเท่าที่คุณต้องการผนวกแล้วแปลงกลับเป็นรายการเมื่อคุณทำเสร็จแล้ว นี้เป็นอย่างใดคล้ายกับสิ่งที่คุณทำเมื่อคุณใช้StringBuilderในการสร้างสตริงและในตอนท้ายได้รับผลเป็นString(ไม่เปลี่ยนรูป!) toStringโดยการเรียก สิ่งที่แตกต่างคือว่า a StringBuilderนั้นไม่แน่นอน แต่รายการที่แตกต่างนั้นไม่เปลี่ยนรูป นอกจากนี้เมื่อคุณแปลงรายการผลต่างกลับไปเป็นรายการคุณยังต้องสร้างรายการใหม่ทั้งหมด แต่อีกครั้งคุณต้องทำสิ่งนี้เพียงครั้งเดียว

มันควรจะค่อนข้างง่ายที่จะดำเนินการเปลี่ยนรูปDListชั้นเรียนที่มีอินเตอร์เฟซที่คล้ายกันเป็นของ Data.DListHaskell


4

คุณต้องดูวิดีโอที่ยอดเยี่ยมนี้ในปี 2015 React conf โดยผู้สร้างของImmutable.js Lee Byron มันจะให้คุณพอยน์เตอร์และโครงสร้างเพื่อทำความเข้าใจวิธีการใช้รายการที่ไม่เปลี่ยนรูปที่มีประสิทธิภาพซึ่งไม่ซ้ำเนื้อหา แนวคิดพื้นฐานคือ: - ตราบเท่าที่สองรายการใช้โหนดที่เหมือนกัน (ค่าเดียวกัน, โหนดถัดไปเดียวกัน), โหนดเดียวกันนั้นถูกใช้ - เมื่อรายการเริ่มแตกต่างกันโครงสร้างจะถูกสร้างขึ้นที่โหนดของความแตกต่างซึ่งถือตัวชี้ไปที่ โหนดเฉพาะถัดไปของแต่ละรายการ

ภาพจากการกวดวิชาการตอบโต้นี้อาจจะชัดเจนกว่าภาษาอังกฤษที่เสีย:ป้อนคำอธิบายรูปภาพที่นี่


2

มันไม่ได้เป็นจาวาอย่างเคร่งครัด แต่ฉันขอแนะนำให้คุณอ่านบทความนี้เกี่ยวกับโครงสร้างข้อมูลถาวรที่ไม่เปลี่ยนรูป แต่มีประสิทธิภาพซึ่งจัดทำดัชนีใน Scala:

http://www.codecommit.com/blog/scala/implementing-persistent-vectors-in-scala

เนื่องจากมันเป็นโครงสร้างข้อมูล Scala จึงสามารถใช้จาก Java ได้เช่นกัน (พร้อมการแสดงรายละเอียดเพิ่มเติมเล็กน้อย) มันขึ้นอยู่กับโครงสร้างข้อมูลที่มีอยู่ใน Clojure และฉันแน่ใจว่ามีไลบรารี Java "ดั้งเดิม" มากกว่าที่เสนอให้เช่นกัน

นอกจากนี้ข้อควรทราบเกี่ยวกับการสร้างโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปแบบ: สิ่งที่คุณมักต้องการคือ "ตัวสร้าง" ซึ่งช่วยให้คุณ "กลายพันธุ์" โครงสร้างข้อมูล "ที่อยู่ระหว่างการพัฒนา" โดยผนวกองค์ประกอบเข้าด้วยกัน (ภายในเธรดเดียว) หลังจากเสร็จสิ้นการผนวกคุณเรียกใช้วิธีการในวัตถุ "ภายใต้การก่อสร้าง" เช่น.build()หรือ.result()ที่ "สร้าง" วัตถุให้โครงสร้างข้อมูลที่ไม่เปลี่ยนรูปซึ่งคุณสามารถแบ่งปันได้อย่างปลอดภัย


1
มีคำถามเกี่ยวกับพอร์ต Java ของ PersistentVector ที่stackoverflow.com/questions/15997996/ …
James_pic

1

วิธีการที่เป็นประโยชน์ในบางครั้งจะมีสองชั้นของวัตถุ - รายการ - วัตถุ - รายชื่อไปข้างหน้ากับการfinalอ้างอิงถึงโหนดแรกในรายการที่เชื่อมโยงไปข้างหน้า - และ - โมฆะในตอนแรกไม่ใช่ - finalอ้างอิงซึ่งเมื่อ - เป็นโมฆะ) ระบุวัตถุย้อนกลับรายการที่ถือรายการเดียวกันในลำดับที่ตรงข้ามและวัตถุรายการย้อนกลับที่มีการfinalอ้างอิงถึงรายการสุดท้ายของรายการย้อนกลับเชื่อมโยงและการอ้างอิงเริ่มต้นเป็นโมฆะไม่ใช่อ้างอิงสุดท้ายซึ่งเมื่อ - null) ระบุวัตถุไปข้างหน้ารายการถือรายการเดียวกันในลำดับที่ตรงข้าม

การจัดเตรียมไอเท็มไปยังรายการไปข้างหน้าหรือต่อท้ายรายการไปยังรายการย้อนกลับจะต้องสร้างโหนดใหม่ซึ่งลิงก์ไปยังโหนดที่ระบุโดยการfinalอ้างอิงและการสร้างวัตถุรายการใหม่ที่เป็นชนิดเดียวกันกับต้นฉบับโดยมีการfinalอ้างอิง ไปยังโหนดใหม่นั้น

การต่อท้ายไอเท็มกับรายการไปข้างหน้าหรือการเพิ่มในรายการย้อนกลับจะต้องมีรายการประเภทตรงกันข้าม ครั้งแรกที่ทำรายการเฉพาะควรสร้างวัตถุใหม่ของประเภทตรงกันข้ามและเก็บการอ้างอิง การทำซ้ำการกระทำควรใช้รายการซ้ำ

โปรดทราบว่าสถานะภายนอกของวัตถุรายการจะได้รับการพิจารณาเหมือนกันไม่ว่าจะเป็นการอ้างอิงไปยังรายการประเภทตรงกันข้ามเป็นโมฆะหรือระบุรายการคำสั่งซื้อตรงกันข้าม ไม่จำเป็นต้องสร้างตัวแปรใด ๆfinalแม้ว่าจะใช้รหัสแบบมัลติเธรดเนื่องจากวัตถุทุกรายการจะมีการfinalอ้างอิงไปยังสำเนาของเนื้อหาทั้งหมด

หากโค้ดในหนึ่งเธรดสร้างและแคชสำเนาที่กลับรายการและฟิลด์ที่สำเนานั้นแคชไม่ผันผวนอาจเป็นไปได้ว่าโค้ดในเธรดอื่นอาจไม่เห็นรายการแคช แต่ผลกระทบที่ไม่พึงประสงค์เท่านั้น จากนั้นจะเป็นที่หัวข้ออื่น ๆ จะต้องทำงานพิเศษสร้างอาคารอีกรายการที่กลับรายการ เพราะการกระทำดังกล่าวจะทำให้ประสิทธิภาพvolatileด้อยลงแต่จะไม่ส่งผลกระทบต่อความถูกต้องและเนื่องจากตัวแปรสร้างความบกพร่องด้านประสิทธิภาพของตนเองจึงมักจะดีกว่าหากมีตัวแปรที่ไม่ลบเลือนและยอมรับความเป็นไปได้ของการดำเนินการซ้ำซ้อนเป็นครั้งคราว

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.