เหตุใดจึงต้องเริ่ม ArrayList ด้วยความจุเริ่มต้น


149

ตัวสร้างตามปกติของArrayListคือ:

ArrayList<?> list = new ArrayList<>();

แต่ยังมีคอนสตรัคเตอร์ที่โอเวอร์โหลดพร้อมพารามิเตอร์สำหรับความจุเริ่มต้น:

ArrayList<?> list = new ArrayList<>(20);

ทำไมจึงเป็นประโยชน์ในการสร้างArrayListความจุเริ่มต้นเมื่อเราสามารถผนวกเข้ากับมันได้ตามที่เราต้องการ?


17
คุณได้ลองดูซอร์สโค้ด ArrayList แล้วหรือยัง
AmitG

@ โจอาคิมซาวเออร์: บางครั้งเราก็รับรู้เมื่อเราอ่านแหล่งข้อมูลอย่างระมัดระวัง ฉันลองทำดูถ้าเขาอ่านต้นฉบับแล้ว ฉันเข้าใจแง่มุมของคุณ ขอบคุณ
AmitG

ArrayList เป็นช่วงเวลาทำงานที่มีประสิทธิภาพไม่ดีทำไมคุณต้องการใช้โครงสร้างดังกล่าว
PositiveGuy

คำตอบ:


196

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

ยิ่งรายการสุดท้ายมีขนาดใหญ่เท่าใดคุณก็ยิ่งประหยัดเวลามากขึ้นเท่านั้นโดยหลีกเลี่ยงการจัดสรรใหม่

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


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

2
@PeterOlson O(n log n)จะทำเวลาlog nทำงาน nนั่นคือการประเมินค่าสูงเกินไปขั้นต้น (แม้ว่าถูกต้องทางเทคนิคด้วย O ใหญ่เพราะมันเป็นขอบเขตบน) มันคัดลอก s + s * 1.5 + s * 1.5 ^ 2 + ... + s * 1.5 ^ m (เช่นที่ * 1.5 ^ m <n <s * 1.5 ^ (m + 1) องค์ประกอบทั้งหมด ฉันไม่เก่งเรื่องผลบวกฉันจึงไม่สามารถให้คณิตศาสตร์ที่แม่นยำกับส่วนหัวของคุณ (สำหรับการปรับขนาดตัวคูณ 2 มันคือ 2n ดังนั้นมันอาจจะ 1.5n ให้หรือให้ค่าคงตัวเล็ก ๆ ) แต่มันก็ไม่ได้ t ใช้เวลามากเกินไป squinting เพื่อดูว่าผลรวมนี้เป็นปัจจัยคงที่ที่ใหญ่กว่า n ดังนั้นจึงต้องใช้ O (k * n) สำเนาซึ่งแน่นอนว่า O (n)

1
@delnan: ไม่สามารถโต้เถียงกับที่! ;) BTW ฉันชอบการโต้เถียงของคุณจริงๆ จะเพิ่มลงในเพลงของฉัน
NPE

6
การโต้แย้งง่ายขึ้นด้วยการเสแสร้ง สมมติว่าคุณเป็นสองเท่าเมื่อเต็มเริ่มด้วยองค์ประกอบเดียว สมมติว่าคุณต้องการแทรก 8 องค์ประกอบ แทรกหนึ่งรายการ (ราคา: 1) แทรกสอง - สองเท่าคัดลอกหนึ่งองค์ประกอบและใส่สองรายการ (ราคา: 2) แทรกสาม - สองเท่าคัดลอกสององค์ประกอบแทรกสาม (ราคา: 3) แทรกสี่ (ราคา: 1) แทรกห้า - สองเท่าคัดลอกสี่องค์ประกอบแทรกห้า (ราคา: 5) แทรกหกเจ็ดและแปด (ราคา: 3) ต้นทุนทั้งหมด: 1 + 2 + 3 + 1 + 5 + 3 = 16 ซึ่งเป็นสองเท่าของจำนวนองค์ประกอบที่แทรก จากภาพร่างนี้คุณสามารถพิสูจน์ได้ว่าต้นทุนเฉลี่ยคือสองต่อการแทรกโดยทั่วไป
Eric Lippert

9
นั่นคือค่าใช้จ่ายในเวลา นอกจากนี้คุณยังสามารถดูได้ว่าปริมาณพื้นที่ที่สูญเปล่าเปลี่ยนไปตามเวลาเป็น 0% ของเวลาและใกล้เคียงกับ 100% ของเวลา การเปลี่ยนปัจจัยจาก 2 เป็น 1.5 หรือ 4 หรือ 100 หรืออะไรก็ตามที่เปลี่ยนแปลงปริมาณพื้นที่ว่างเปล่าโดยเฉลี่ยและจำนวนเวลาเฉลี่ยที่ใช้ในการคัดลอก แต่ความซับซ้อนของเวลายังคงเป็นเส้นตรงโดยเฉลี่ยไม่ว่าปัจจัยนั้นคืออะไร
Eric Lippert

41

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

ดังนั้นถ้าคุณรู้ว่าขอบเขตบนของคุณคือ 20 รายการการสร้างอาร์เรย์ที่มีความยาวเริ่มต้นเท่ากับ 20 จะดีกว่าการใช้ค่าเริ่มต้นเท่ากับ 15 แล้วปรับขนาดเป็น 15*2 = 30และใช้เพียง 20 เท่านั้นในขณะที่สิ้นเปลืองวงจรสำหรับการขยาย

ป.ล. - ในฐานะที่ AmitG กล่าวว่าปัจจัยการขยายตัวคือการดำเนินการเฉพาะ (ในกรณีนี้(oldCapacity * 3)/2 + 1)


9
เป็นจริงint newCapacity = (oldCapacity * 3)/2 + 1;
AmitG

25

ขนาดเริ่มต้นของ ArrayList คือ10

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
    this(10);
    } 

ดังนั้นหากคุณกำลังจะเพิ่ม 100 หรือมากกว่าระเบียนคุณสามารถดูค่าใช้จ่ายของการจัดสรรหน่วยความจำ

ArrayList<?> list = new ArrayList<>();    
// same as  new ArrayList<>(10);      

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


ไม่มีการรับประกันว่าความจุเริ่มต้นจะเป็น 10 สำหรับรุ่น JDK ในอนาคตเสมอ -private static final int DEFAULT_CAPACITY = 10
vikingsteve

17

ที่จริงฉันเขียนโพสต์บล็อกในหัวข้อ 2 เดือนที่ผ่านมา บทความนี้ใช้สำหรับ C # List<T>แต่ Java ArrayListมีการใช้งานที่คล้ายกันมาก เนื่องจากArrayListมีการใช้งานโดยใช้อาร์เรย์แบบไดนามิกจึงเพิ่มขนาดตามความต้องการ ดังนั้นเหตุผลสำหรับตัวสร้างกำลังการผลิตเพื่อวัตถุประสงค์ในการเพิ่มประสิทธิภาพ

เมื่อหนึ่งในการดำเนินการปรับขนาดเหล่านี้เกิดขึ้น ArrayList จะคัดลอกเนื้อหาของอาร์เรย์ลงในอาร์เรย์ใหม่ที่มีความจุเป็นสองเท่าของอาร์เรย์เก่า การดำเนินการนี้วิ่งในO (n)เวลา

ตัวอย่าง

นี่คือตัวอย่างของการArrayListเพิ่มขนาด:

10
16
25
38
58
... 17 resizes ...
198578
297868
446803
670205
1005308

ดังนั้นรายการเริ่มต้นที่มีความจุ10เมื่อรายการที่ 11 จะมีการเพิ่มมันคือการเพิ่มขึ้นโดยการ50% + 1 16ในรายการที่ 17 ArrayListจะเพิ่มขึ้นอีกครั้งเพื่อ251000000ตอนนี้พิจารณาตัวอย่างที่เรากำลังสร้างรายการที่มีความจุที่ต้องการเป็นที่รู้จักกันอยู่แล้วเป็น การสร้างตัวสร้างแบบArrayListไม่มีขนาดจะเรียกArrayList.add 1000000เวลาที่ใช้O (1)ตามปกติหรือO (n)ในการปรับขนาด

1000000 + 16 + 25 + ... + 670205 + 1005308 = 4015851

เปรียบเทียบนี้ใช้สร้างแล้วโทรArrayList.addซึ่งรับประกันได้ว่าจะทำงานในO (1)

1000000 + 1000000 = 2000000 การดำเนินการ

Java เทียบกับ C #

Java เป็นข้างต้นเริ่มต้นที่ 1050% + 1และเพิ่มขึ้นในแต่ละที่ปรับขนาด C # เริ่มต้น4และเพิ่มขึ้นอย่างก้าวร้าวมากขึ้นเป็นสองเท่าในการปรับขนาดแต่ละครั้ง 1000000เพิ่มตัวอย่างจากข้างต้นสำหรับ C # ใช้3097084การดำเนินงาน

อ้างอิง


9

การตั้งค่าขนาดเริ่มต้นของ ArrayList เช่นถึง ArrayList<>(100)ลดจำนวนครั้งที่การจัดสรรหน่วยความจำภายในจะเกิดขึ้น

ตัวอย่าง:

ArrayList example = new ArrayList<Integer>(3);
example.add(1); // size() == 1
example.add(2); // size() == 2, 
example.add(2); // size() == 3, example has been 'filled'
example.add(3); // size() == 4, example has been 'expanded' so that the fourth element can be added. 

ตามที่คุณเห็นในตัวอย่างด้านบน - ArrayListสามารถขยายได้ถ้าจำเป็น สิ่งนี้ไม่แสดงให้คุณเห็นว่าขนาดของ Arraylist นั้นมักจะเพิ่มเป็นสองเท่า (แม้ว่าโปรดทราบว่าขนาดใหม่นั้นขึ้นอยู่กับการใช้งานของคุณ) คำพูดต่อไปนี้มาจากOracle :

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

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


3

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


3

นี่คือเพื่อหลีกเลี่ยงความพยายามที่เป็นไปได้สำหรับการจัดสรรใหม่สำหรับทุกวัตถุเดียว

int newCapacity = (oldCapacity * 3)/2 + 1;

new Object[]ถูกสร้างขึ้นภายใน
JVM ต้องการความพยายามในการสร้าง new Object[]เมื่อคุณเพิ่มองค์ประกอบใน arraylist หากคุณไม่มีรหัสข้างต้น (algo ใด ๆ ที่คุณคิดว่า) สำหรับการจัดสรรใหม่ทุกครั้งที่คุณเรียกใช้arraylist.add()นั้นnew Object[]จะต้องสร้างขึ้นซึ่งไม่มีจุดหมายและเราจะเสียเวลาในการเพิ่มขนาด 1 สำหรับแต่ละวัตถุที่จะเพิ่ม ดังนั้นจึงเป็นการดีกว่าที่จะเพิ่มขนาดObject[]ด้วยสูตรต่อไปนี้
(JSL ใช้สูตร forcasting ที่ระบุด้านล่างสำหรับ arraylist ที่กำลังเติบโตแบบไดนามิกแทนที่จะเติบโต 1 ครั้งทุกครั้งเนื่องจากการเติบโตจะใช้ความพยายามโดย JVM)

int newCapacity = (oldCapacity * 3)/2 + 1;

ArrayList จะไม่ทำการจัดสรรใหม่สำหรับแต่ละรายการadd- มันใช้สูตรการเติบโตบางส่วนแล้วภายใน ดังนั้นคำถามที่ไม่ตอบ
อา.

คำตอบของฉันคือ @AH สำหรับการทดสอบเชิงลบ กรุณาอ่านระหว่างบรรทัด ฉันพูดว่า"ถ้าคุณไม่มีโค้ดด้านบน (algo ใด ๆ ที่คุณคิดว่า) สำหรับการจัดสรรใหม่ทุกครั้งที่คุณเรียกใช้ arraylist.add () ดังนั้น Object [] ใหม่จะต้องถูกสร้างขึ้นซึ่งไม่มีจุดหมายและเราจะเสียเวลา" และรหัสเป็นint newCapacity = (oldCapacity * 3)/2 + 1;ที่มีอยู่ในระดับ ArrayList คุณยังคิดว่ามันยังไม่ได้ตอบ?
AmitG

1
ผมยังคิดว่ามันไม่ได้ตอบ: ในArrayListจัดสรรตัดจำหน่ายจะเกิดขึ้นในใด ๆกรณีที่มีการใด ๆค่าสำหรับความจุเริ่มต้น และคำถามเกี่ยวกับ: ทำไมต้องใช้ค่าที่ไม่ได้มาตรฐานสำหรับความจุเริ่มต้นเลย? นอกจากนี้: "การอ่านระหว่างบรรทัด" ไม่ใช่สิ่งที่ต้องการในคำตอบทางเทคนิค ;-)
AH

@AH ฉันตอบว่าเกิดอะไรขึ้นถ้าเราไม่ได้มีกระบวนการจัดสรรใน ArrayList ดังนั้นคำตอบคือ ลองอ่านวิญญาณของคำตอบ :-) ฉันรู้ดีขึ้น ใน ArrayList การจัดสรรใหม่ที่ตัดจำหน่ายเกิดขึ้นไม่ว่าในกรณีใด ๆ ด้วยค่าใด ๆ
AmitG

2

ฉันคิดว่าแต่ละ ArrayList สร้างขึ้นด้วยค่าความจุเริ่มต้นที่ "10" ดังนั้นถ้าคุณสร้าง ArrayList โดยไม่ตั้งค่าความจุภายใน Constructor มันจะถูกสร้างขึ้นด้วยค่าเริ่มต้น


2

ฉันว่ามันเป็นการเพิ่มประสิทธิภาพ ArrayList ที่ไม่มีความจุเริ่มต้นจะมีแถวว่าง ~ 10 แถวและจะขยายเมื่อคุณทำการเพิ่ม

ในการมีรายการที่มีจำนวนรายการที่แน่นอนคุณต้องเรียกtrimToSize ()


0

ตามประสบการณ์ของฉันด้วยการArrayListให้ความจุเริ่มต้นเป็นวิธีที่ดีในการหลีกเลี่ยงค่าใช้จ่ายในการจัดสรรใหม่ แต่มันมีข้อแม้ ข้อเสนอแนะทั้งหมดที่กล่าวมาข้างต้นบอกว่าควรให้กำลังการผลิตเริ่มต้นเฉพาะเมื่อทราบจำนวนคร่าว ๆ ของจำนวนองค์ประกอบอย่างคร่าวๆ แต่เมื่อเราพยายามที่จะให้ความจุเริ่มต้นโดยไม่มีความคิดจำนวนหน่วยความจำที่สำรองไว้และไม่ได้ใช้จะเสียเพราะมันอาจไม่จำเป็นเมื่อรายการเต็มไปด้วยองค์ประกอบที่ต้องการ สิ่งที่ฉันพูดคือเราสามารถนำไปใช้ในทางปฏิบัติในตอนเริ่มต้นในขณะที่การจัดสรรความจุและจากนั้นหาวิธีที่ชาญฉลาดในการรู้ความจุขั้นต่ำที่ต้องการที่รันไทม์ ArrayList ensureCapacity(int minCapacity)มีวิธีที่เรียกว่า แต่แล้วก็มีวิธีที่ชาญฉลาด ...


0

ฉันได้ทดสอบ ArrayList แบบมีและไม่มี initialCapacity และฉันได้ผลลัพธ์ที่น่าแปลกใจ
เมื่อฉันตั้งค่า LOOP_NUMBER เป็น 100,000 หรือน้อยกว่าผลลัพธ์คือการตั้งค่า initialCapacity นั้นมีประสิทธิภาพ

list1Sttop-list1Start = 14
list2Sttop-list2Start = 10


แต่เมื่อฉันตั้งค่า LOOP_NUMBER เป็น 1,000,000 ผลลัพธ์จะเปลี่ยนเป็น:

list1Stop-list1Start = 40
list2Stop-list2Start = 66


ในที่สุดฉันไม่สามารถคิดออกว่ามันทำงานอย่างไร!
รหัสตัวอย่าง:

 public static final int LOOP_NUMBER = 100000;

public static void main(String[] args) {

    long list1Start = System.currentTimeMillis();
    List<Integer> list1 = new ArrayList();
    for (int i = 0; i < LOOP_NUMBER; i++) {
        list1.add(i);
    }
    long list1Stop = System.currentTimeMillis();
    System.out.println("list1Stop-list1Start = " + String.valueOf(list1Stop - list1Start));

    long list2Start = System.currentTimeMillis();
    List<Integer> list2 = new ArrayList(LOOP_NUMBER);
    for (int i = 0; i < LOOP_NUMBER; i++) {
        list2.add(i);
    }
    long list2Stop = System.currentTimeMillis();
    System.out.println("list2Stop-list2Start = " + String.valueOf(list2Stop - list2Start));
}

ฉันได้ทดสอบกับ windows8.1 และ jdk1.7.0_80


1
สวัสดีขออภัยปัจจุบันความคลาดเคลื่อน TimeTimeMillis มีมากถึงร้อยมิลลิวินาที (ขึ้นอยู่กับ) ซึ่งหมายความว่าผลลัพธ์นั้นแทบจะไม่น่าเชื่อถือ ฉันขอแนะนำให้ใช้ห้องสมุดที่กำหนดเองเพื่อให้ถูกต้อง
Bogdan
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.