0Pricing
Java Academy · บทเรียน

การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ

Java 8 ขึ้นไปรับมือการชนกันอย่างไร

การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ เป็นบทเรียน Java Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Java Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน

ปัญหาการชนกัน

ก่อน Java 8 ช่องที่มีการชนกันจำนวนมากจะกลายเป็นรายการเชื่อมโยงที่ยาว การค้นหาในช่องนั้นจึงมีประสิทธิภาพลดลงเป็น O(n)

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

public class Main {
    public static void main(String[] args) {
        // All these strings can be made to collide in one bucket
        System.out.println("FB".hashCode() == "Ea".hashCode());
    }
}

การแปลงเป็นต้นไม้ใน Java 8

Java 8 เพิ่มการแปลงเป็นต้นไม้ เมื่อช่องใดช่องหนึ่งมีรายการมากเกินไป รายการเชื่อมโยงจะถูกแปลงเป็นต้นไม้แดง-ดำแบบสมดุล

การค้นหาในช่องนั้นจึงกลายเป็น O(log n) แทนที่จะเป็น O(n)

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<Integer, Integer> m = new HashMap<>();
        for (int i = 0; i < 1000; i++) m.put(i, i);
        System.out.println("Lookups stay fast: " + m.get(742));
    }
}

เกณฑ์: TREEIFY_THRESHOLD

ค่าคงที่ TREEIFY_THRESHOLD คือ8 ช่องจะถูกแปลงเป็นต้นไม้เมื่อมีรายการครบ 8 รายการ

แต่ยังมีเงื่อนไขที่สองด้วย นั่นคือตารางต้องมีขนาดอย่างน้อย MIN_TREEIFY_CAPACITY (64) มิฉะนั้นแมปจะปรับขนาดแทน

public class Main {
    public static void main(String[] args) {
        int TREEIFY_THRESHOLD = 8;
        int MIN_TREEIFY_CAPACITY = 64;
        System.out.println("Treeify when bucket size >= " + TREEIFY_THRESHOLD);
        System.out.println("...and table capacity >= " + MIN_TREEIFY_CAPACITY);
    }
}

ปรับขนาดก่อน แล้วจึงแปลงเป็นต้นไม้

หากช่องมีรายการเกินขนาด แต่ตารางยังมีขนาดเล็ก (น้อยกว่า 64) HashMap จะปรับขนาดตารางก่อน

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

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<Integer, Integer> m = new HashMap<>(16);
        for (int i = 0; i < 50; i++) m.put(i, i);
        // Many resizes happened before any treeify would
        System.out.println("size = " + m.size());
    }
}

การแปลงกลับจากต้นไม้

ต้นไม้ไม่ได้คงอยู่ถาวร หากการลบรายการทำให้ช่องมีจำนวนน้อยกว่า UNTREEIFY_THRESHOLD (6) ต้นไม้จะถูกเปลี่ยนกลับเป็นรายการเชื่อมโยง

ช่องว่างระหว่าง 8 (แปลงเป็นต้นไม้) และ 6 (แปลงกลับ) ช่วยหลีกเลี่ยงการสลับไปมาที่ขอบเขต

public class Main {
    public static void main(String[] args) {
        System.out.println("TREEIFY_THRESHOLD   = 8");
        System.out.println("UNTREEIFY_THRESHOLD = 6");
        System.out.println("Gap prevents flip-flopping at the edge");
    }
}

ต้นไม้ต้องใช้ลำดับจากการเปรียบเทียบหรือเอกลักษณ์

ต้นไม้แดง-ดำต้องจัดลำดับรายการของตัวเอง HashMap จะเปรียบเทียบค่าแฮชก่อน หากค่าเท่ากันจะใช้ Comparable หากคีย์นำไปใช้งานได้ หรือใช้การตัดสินกรณีเสมอที่คงที่โดยอาศัยชื่อคลาสและเอกลักษณ์ของอ็อบเจ็กต์

คีย์ที่เป็น Comparable (เช่น String หรือชนิดจำนวนเต็ม) จะให้ลำดับในต้นไม้ที่ชัดเจนที่สุด

public class Main {
    public static void main(String[] args) {
        System.out.println("String is Comparable: " + ("a" instanceof Comparable));
        System.out.println("Integer is Comparable: " + (Integer.valueOf(1) instanceof Comparable));
    }
}

ผลกระทบในทางปฏิบัติ

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

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

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> m = new HashMap<>();
        m.put("alpha", 1);
        m.put("beta", 2);
        m.put("gamma", 3);
        // Tiny buckets, plain linked lists, no trees needed
        System.out.println(m.get("beta"));
    }
}

hashCode ค่าคงที่บังคับให้เกิดต้นไม้

หากคุณจงใจส่งคืน hashCode ค่าคงที่ คีย์ทุกตัวจะไปอยู่ในช่องเดียว เมื่อความจุมีค่า 64 ขึ้นไป ช่องนั้นจะถูกแปลงเป็นต้นไม้

นี่แสดงให้เห็นกลไกป้องกัน แต่เป็นสัญญาณว่าการออกแบบมีปัญหา ควรแก้ไข hashCode แทน

import java.util.HashMap;
import java.util.Map;

public class Main {
    static class Bad implements Comparable<Bad> {
        final int v;
        Bad(int v) { this.v = v; }
        @Override public int hashCode() { return 1; } // forces collisions
        @Override public boolean equals(Object o) { return o instanceof Bad b && b.v == v; }
        @Override public int compareTo(Bad o) { return Integer.compare(v, o.v); }
    }
    public static void main(String[] args) {
        Map<Bad, Integer> m = new HashMap<>();
        for (int i = 0; i < 100; i++) m.put(new Bad(i), i);
        System.out.println("All in one bucket, still works: " + m.get(new Bad(50)));
    }
}

ต้นไม้ใช้หน่วยความจำมาก

โหนดต้นไม้มีขนาดใหญ่กว่าโหนดรายการเชื่อมโยงทั่วไป เพราะเก็บการอ้างอิงไปยังโหนดแม่ โหนดซ้าย โหนดขวา และสี

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

public class Main {
    public static void main(String[] args) {
        System.out.println("Node: hash, key, value, next");
        System.out.println("TreeNode: + parent, left, right, prev, red flag");
        System.out.println("=> trees cost more memory per entry");
    }
}

วิธีหลีกเลี่ยงการแปลงเป็นต้นไม้

คุณแทบจะไม่ต้องการพึ่งพาการแปลงเป็นต้นไม้ ควรหลีกเลี่ยงโดย:

  • เขียน hashCode() ที่กระจายได้ดี
  • ใช้ชนิดข้อมูลในตัวหรือระเบียนเป็นคีย์
  • กำหนดขนาดแมปล่วงหน้าเพื่อลดการชนกัน
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Main {
    record Key(int a, int b) {}
    public static void main(String[] args) {
        Map<Key, Integer> m = new HashMap<>(256);
        for (int i = 0; i < 200; i++) m.put(new Key(i, i * 31), i);
        System.out.println("Even distribution, fast lookups: " + m.get(new Key(10, 310)));
    }
}

สรุปประสิทธิภาพ

ต้นทุนการดำเนินการของ HashMap:

  • แฮชที่ดี: O(1) โดยเฉลี่ย
  • ช่องแบบรายการเชื่อมโยง: O(n) ต่อช่องในกรณีเลวร้ายที่สุด
  • ช่องที่แปลงเป็นต้นไม้: O(log n) ต่อช่อง

การแปลงเป็นต้นไม้ช่วยจำกัดกรณีเลวร้ายที่สุด แต่ hashCode ที่ดีจะทำให้คุณยังคงอยู่ในระดับ O(1)

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<Integer, Integer> m = new HashMap<>(1 << 14);
        for (int i = 0; i < 10000; i++) m.put(i, i);
        System.out.println("10k entries, O(1) get: " + m.get(9999));
    }
}

ตรวจสอบความเข้าใจ

ทดสอบความรู้ของคุณเกี่ยวกับการแปลงเป็นต้นไม้

ทบทวน

คุณได้เรียนรู้วิธีที่ HashMap สมัยใหม่จัดการการชนกัน:

  • ช่องจะแปลงเป็นต้นไม้เมื่อมี 8 รายการและความจุอย่างน้อย 64
  • ต้นไม้ทำให้การค้นหาในกรณีเลวร้ายที่สุดเป็นO(log n)
  • ช่องจะแปลงกลับจากต้นไม้เมื่อมีรายการน้อยกว่า 6 รายการ
  • hashCode ที่ดีหมายความว่าคุณแทบไม่ต้องใช้กลไกป้องกันนี้

คุณเรียนจบหลักสูตรโครงสร้างภายในของ HashMap แล้ว

public class Main {
    public static void main(String[] args) {
        System.out.println("Treeification course complete");
    }
}

คำถามที่พบบ่อย

บทเรียน “การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Java Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ”

Java 8 ขึ้นไปรับมือการชนกันอย่างไร คุณปฏิบัติ Java Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Java Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Java Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Java Academy นี้ได้ไหม

ได้ บทเรียน Java Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การทำงานของ HashMap
  2. สัญญาของ equals/hashCode
  3. การสร้าง hashCode
  4. การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ
← กลับไปที่ Java Academy