HashMap รับ / ใส่ความซับซ้อน


132

เราเคยพูดว่าHashMap get/putการดำเนินการคือ O (1) อย่างไรก็ตามขึ้นอยู่กับการใช้งานแฮช แฮชของอ็อบเจ็กต์ดีฟอลต์คือแอดเดรสภายในในฮีป JVM เราแน่ใจหรือไม่ว่าดีพอที่จะอ้างว่าget/putเป็น O (1)?

หน่วยความจำที่ใช้ได้เป็นอีกปัญหาหนึ่ง ตามที่ฉันเข้าใจจาก javadocs HashMap load factorควรเป็น 0.75 จะเกิดอะไรขึ้นถ้าเรามีหน่วยความจำไม่เพียงพอใน JVM และload factorเกินขีด จำกัด ?

ดังนั้นดูเหมือนว่าจะไม่รับประกัน O (1) มันสมเหตุสมผลหรือว่าฉันพลาดอะไรไป


1
คุณอาจต้องการค้นหาแนวคิดของความซับซ้อนในการตัดจำหน่าย ดูตัวอย่างได้ที่นี่: stackoverflow.com/questions/3949217/time-complexity-of-hash-table ความซับซ้อนของกรณีที่เลวร้ายที่สุดไม่ใช่มาตรการที่สำคัญที่สุดสำหรับตารางแฮช
Dr G

3
ถูกต้อง - ตัดจำหน่าย O (1) - อย่าลืมส่วนแรกนั้นและคุณจะไม่มีคำถามประเภทนี้ :)
วิศวกร

ความซับซ้อนของเวลากรณีที่แย่ที่สุดคือ O (logN) ตั้งแต่ Java 1.8 ถ้าฉันไม่ผิด
Tarun Kolla

คำตอบ:


216

มันขึ้นอยู่กับหลาย ๆ อย่าง โดยปกติจะเป็น O (1) โดยมีแฮชที่เหมาะสมซึ่งเป็นเวลาคงที่ ... แต่คุณอาจมีแฮชที่ใช้เวลาคำนวณนานและหากมีหลายรายการในแผนที่แฮชที่ส่งคืนรหัสแฮชเดียวกันgetจะต้องวนซ้ำเพื่อเรียกร้องให้equalsแต่ละคนหาคู่

ในกรณีที่เลวร้ายที่สุด a HashMapมีการค้นหา O (n) เนื่องจากการเดินผ่านรายการทั้งหมดในที่เก็บแฮชเดียวกัน (เช่นหากพวกเขาทั้งหมดมีรหัสแฮชเดียวกัน) โชคดีที่เหตุการณ์ที่เลวร้ายที่สุดนั้นไม่ได้เกิดขึ้นบ่อยนักในชีวิตจริงจากประสบการณ์ของฉัน ดังนั้นจึงไม่รับประกัน O (1) อย่างแน่นอน - แต่โดยปกติแล้วคุณควรพิจารณาเมื่อพิจารณาว่าจะใช้อัลกอริทึมและโครงสร้างข้อมูลใด

ใน JDK 8 HashMapได้รับการปรับแต่งเพื่อให้สามารถเปรียบเทียบคีย์สำหรับการสั่งซื้อได้จากนั้นที่เก็บข้อมูลที่มีประชากรหนาแน่นจะถูกนำไปใช้เป็นแบบต้นไม้ดังนั้นแม้ว่าจะมีรายการจำนวนมากที่มีรหัสแฮชเดียวกัน แต่ความซับซ้อนก็คือ O (log n) ซึ่งอาจทำให้เกิดปัญหาได้หากคุณมีประเภทคีย์ที่ความเท่าเทียมกันและลำดับต่างกันแน่นอน

และใช่ถ้าคุณมีหน่วยความจำไม่เพียงพอสำหรับแผนที่แฮชคุณจะมีปัญหา ... แต่นั่นจะเป็นจริงตามโครงสร้างข้อมูลที่คุณใช้


@marcog: คุณถือว่า O (n log n) สำหรับการค้นหาครั้งเดียวหรือไม่? นั่นฟังดูบ้าสำหรับฉัน แน่นอนว่าจะขึ้นอยู่กับความซับซ้อนของฟังก์ชันแฮชและความเท่าเทียมกัน แต่ไม่น่าจะขึ้นอยู่กับขนาดของแผนที่
Jon Skeet

1
@marcog: แล้วคุณสมมติว่าเป็น O (n log n) คืออะไร? แทรก n รายการ?
Jon Skeet

1
+1 สำหรับคำตอบที่ดี คุณช่วยกรุณาให้ลิงค์เช่นรายการวิกิพีเดียนี้สำหรับตารางแฮชในคำตอบของคุณหรือไม่? ด้วยวิธีนี้ยิ่งผู้อ่านที่สนใจมากขึ้นก็สามารถเข้าใจถึงเหตุผลที่คุณให้คำตอบได้
David Weiser

2
@SleimanJneidi: ยังคงเป็นอยู่ถ้าคีย์ไม่ใช้ Comparable <T> `- แต่ฉันจะอัปเดตคำตอบเมื่อฉันมีเวลามากขึ้น
Jon Skeet

1
@ ip696: ใช่putคือ "ตัดจำหน่าย O (1)" - โดยปกติคือ O (1) บางครั้ง O (n) - แต่ไม่ค่อยเพียงพอที่จะทำให้สมดุล
Jon Skeet

9

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

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

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


6
บ้า ฉันเลือกที่จะเชื่อว่าถ้าฉันไม่ต้องพิมพ์สิ่งนี้บนหน้าจอสัมผัสของโทรศัพท์มือถือที่พลิกได้ฉันก็สามารถเอาชนะ Jon Sheet ได้ มีตราสำหรับสิ่งนั้นใช่ไหม?
Tom Anderson

8

การทำงานของ HashMap ขึ้นอยู่กับปัจจัยของการใช้งาน hashCode สำหรับสถานการณ์ในอุดมคติสมมติว่าการใช้แฮชที่ดีซึ่งให้รหัสแฮชที่ไม่ซ้ำกันสำหรับทุกออบเจ็กต์ (ไม่มีการชนกันของแฮช) จากนั้นสถานการณ์กรณีที่ดีที่สุดเลวร้ายที่สุดและโดยเฉลี่ยจะเป็น O (1) ลองพิจารณาสถานการณ์ที่การใช้งาน hashCode ไม่ถูกต้องจะส่งคืน 1 หรือแฮชที่มีการชนกันของแฮชเสมอ ในกรณีนี้ความซับซ้อนของเวลาจะเป็น O (n)

ตอนนี้มาถึงส่วนที่สองของคำถามเกี่ยวกับหน่วยความจำแล้วใช่ข้อ จำกัด ของหน่วยความจำจะได้รับการดูแลโดย JVM


8

มีการกล่าวไว้แล้วว่าแฮชแมปเป็นO(n/m)ค่าเฉลี่ยถ้าnเป็นจำนวนรายการและmมีขนาด นอกจากนี้ยังได้รับการกล่าวถึงว่าโดยหลักการแล้วสิ่งทั้งหมดสามารถยุบลงในรายการที่เชื่อมโยงกันโดยO(n)ใช้เวลาสืบค้น (ทั้งหมดนี้ถือว่าการคำนวณแฮชเป็นเวลาคงที่)

อย่างไรก็ตามสิ่งที่ไม่ได้กล่าวถึงบ่อยครั้งคือด้วยความน่าจะเป็นอย่างน้อย1-1/n(ดังนั้นสำหรับ 1,000 รายการที่มีโอกาส 99.9%) ถังที่ใหญ่ที่สุดจะไม่ถูกเติมเกินO(logn)! ดังนั้นการจับคู่ความซับซ้อนโดยเฉลี่ยของต้นไม้ค้นหาแบบไบนารี (และค่าคงที่ดีขอบเขตที่แน่นกว่าคือ(log n)*(m/n) + O(1) )

สิ่งที่จำเป็นสำหรับขอบเขตทางทฤษฎีนี้ก็คือคุณใช้ฟังก์ชันแฮชที่ดีพอสมควร (ดู Wikipedia: Universal Hashingอาจทำได้ง่ายๆเพียงa*x>>m ) และแน่นอนว่าคนที่ให้ค่าแก่คุณกับแฮชไม่รู้ว่าคุณเลือกค่าคงที่แบบสุ่มของคุณอย่างไร

TL; DR: มีโอกาสสูงมากที่เลวร้ายที่สุดกรณีที่รับ / วางความซับซ้อนของ HashMap O(logn)เป็น


(และสังเกตว่าไม่มีข้อมูลใดที่ถือว่าเป็นข้อมูลสุ่มความน่าจะเป็นเกิดขึ้นจากการเลือกฟังก์ชันแฮชล้วนๆ)
Thomas Ahle

ฉันยังมีคำถามเดียวกันเกี่ยวกับความซับซ้อนของรันไทม์ของการค้นหาในแผนที่แฮช ดูเหมือนว่าจะเป็น O (n) เนื่องจากปัจจัยคงที่ควรจะลดลง 1 / m เป็นปัจจัยคงที่ดังนั้นจึงหลุดออกจาก O (n)
nickdu

4

ฉันเห็นด้วยกับ:

  • ความซับซ้อนของการตัดจำหน่ายทั่วไปของ O (1)
  • การhashCode()ใช้งานที่ไม่ดีอาจส่งผลให้เกิดการชนกันหลายครั้งซึ่งหมายความว่าในกรณีที่เลวร้ายที่สุดทุกออบเจ็กต์จะไปที่ที่เก็บข้อมูลเดียวกันดังนั้น O ( N ) หากแต่ละที่เก็บข้อมูลได้รับการสนับสนุนโดยกList.
  • ตั้งแต่ Java 8 HashMapจะแทนที่โหนด (รายการที่เชื่อมโยง) แบบไดนามิกที่ใช้ในแต่ละที่เก็บข้อมูลด้วย TreeNodes (ต้นไม้สีแดงดำเมื่อรายการมีขนาดใหญ่กว่า 8 องค์ประกอบ) ส่งผลให้ O ( logN ) มีประสิทธิภาพแย่ที่สุด

แต่นี่ไม่ใช่ความจริงทั้งหมดหากเราต้องการให้แม่นยำ 100% การใช้งานhashCode()และประเภทของคีย์Object (ไม่เปลี่ยนรูป / แคชหรือเป็นคอลเล็กชัน) อาจส่งผลต่อความซับซ้อนจริงในเงื่อนไขที่เข้มงวด

สมมติสามกรณีต่อไปนี้:

  1. HashMap<Integer, V>
  2. HashMap<String, V>
  3. HashMap<List<E>, V>

มีความซับซ้อนเหมือนกันหรือไม่? ความซับซ้อนตัดจำหน่ายของอันที่ 1 เป็นไปตามที่คาดไว้ O (1) แต่ที่เหลือเราต้องคำนวณhashCode()องค์ประกอบการค้นหาด้วยซึ่งหมายความว่าเราอาจต้องสำรวจอาร์เรย์และรายการในอัลกอริทึมของเรา

ให้คิดว่าขนาดของทั้งหมดของอาร์เรย์ดังกล่าวข้างต้น / รายการคือk จากนั้นHashMap<String, V>และHashMap<List<E>, V>จะมี O (k) ตัดจำหน่ายความซับซ้อนและในทำนองเดียวกัน O ( k + logN ) กรณีที่เลวร้ายที่สุดใน Java8

* โปรดทราบว่าการใช้Stringคีย์เป็นกรณีที่ซับซ้อนกว่าเนื่องจากไม่เปลี่ยนรูปและ Java จะแคชผลลัพธ์ของhashCode()ตัวแปรส่วนตัวhashดังนั้นจึงคำนวณเพียงครั้งเดียว

/** Cache the hash code for the string */
    private int hash; // Default to 0

แต่ข้างต้นก็มีกรณีที่เลวร้ายที่สุดเช่นกันเนื่องจากString.hashCode()การใช้งานJava กำลังตรวจสอบว่าhash == 0ก่อนที่จะคำนวณhashCodeหรือไม่ แต่เดี๋ยวก่อนมีสตริงที่ไม่ว่างเปล่าที่ส่งออกเป็นhashcodeศูนย์เช่น "f5a5a608" ดูที่นี่ซึ่งในกรณีนี้การบันทึกช่วยจำอาจไม่เป็นประโยชน์


2

ในทางปฏิบัติมันคือ O (1) แต่จริงๆแล้วนี่เป็นการทำให้เข้าใจง่ายและแย่มากในเชิงคณิตศาสตร์ สัญกรณ์ O () บอกว่าอัลกอริทึมทำงานอย่างไรเมื่อขนาดของปัญหามีแนวโน้มที่จะไม่มีที่สิ้นสุด Hashmap get / put ทำงานเหมือนอัลกอริทึม O (1) สำหรับขนาดที่ จำกัด ขีด จำกัด มีขนาดใหญ่พอสมควรจากหน่วยความจำคอมพิวเตอร์และจากมุมมองที่กำหนดแอดเดรส แต่อยู่ไกลจากระยะอนันต์

เมื่อมีคนบอกว่า hashmap get / put คือ O (1) ควรบอกว่าเวลาที่ต้องการรับ / ใส่นั้นคงที่มากหรือน้อยและไม่ได้ขึ้นอยู่กับจำนวนองค์ประกอบในแฮชแมปเท่าที่แฮชแมปจะทำได้ นำเสนอในระบบคอมพิวเตอร์จริง หากปัญหาเกินขนาดนั้นและเราต้องการแฮชแมปที่ใหญ่ขึ้นหลังจากนั้นไม่นานจำนวนบิตที่อธิบายองค์ประกอบหนึ่งก็จะเพิ่มขึ้นด้วยเมื่อเราใช้องค์ประกอบต่างๆที่อธิบายได้หมด ตัวอย่างเช่นหากเราใช้แฮชแมปเพื่อจัดเก็บตัวเลข 32 บิตและต่อมาเราจะเพิ่มขนาดปัญหาเพื่อที่เราจะมีองค์ประกอบมากกว่า 2 ^ 32 บิตในแฮชแมปองค์ประกอบแต่ละรายการจะถูกอธิบายด้วยมากกว่า 32 บิต

จำนวนบิตที่จำเป็นในการอธิบายแต่ละองค์ประกอบคือ log (N) โดยที่ N คือจำนวนองค์ประกอบสูงสุดดังนั้น get และ put จึงเป็น O (log N) จริงๆ

หากคุณเปรียบเทียบกับชุดต้นไม้ซึ่งก็คือ O (log n) ชุดแฮชจะเป็น O (long (max (n)) และเราก็รู้สึกว่านี่คือ O (1) เนื่องจากในการใช้งานบางอย่าง max (n) ได้รับการแก้ไขไม่เปลี่ยนแปลง (ขนาดของวัตถุที่เราจัดเก็บวัดเป็นบิต) และอัลกอริทึมที่คำนวณรหัสแฮชนั้นรวดเร็ว

สุดท้ายหากพบองค์ประกอบในโครงสร้างข้อมูลใด ๆ เป็น O (1) เราจะสร้างข้อมูลจากอากาศบาง ๆ การมีโครงสร้างข้อมูลขององค์ประกอบ n ฉันสามารถเลือกองค์ประกอบหนึ่งด้วยวิธีที่แตกต่างกัน ด้วยเหตุนี้ฉันจึงสามารถเข้ารหัสข้อมูลบิต log (n) ถ้าฉันสามารถเข้ารหัสเป็นศูนย์บิต (นั่นคือสิ่งที่ O (1) หมายถึง) ฉันก็สร้างอัลกอริทึม ZIP ที่บีบอัดได้ไม่สิ้นสุด


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