হ্যাশসেট কোডের জন্য অপ্রত্যাশিত চলমান সময়


28

সুতরাং মূলত, আমার এই কোডটি ছিল:

import java.util.*;

public class sandbox {
    public static void main(String[] args) {
        HashSet<Integer> hashSet = new HashSet<>();
        for (int i = 0; i < 100_000; i++) {
            hashSet.add(i);
        }

        long start = System.currentTimeMillis();

        for (int i = 0; i < 100_000; i++) {
            for (Integer val : hashSet) {
                if (val != -1) break;
            }

            hashSet.remove(i);
        }

        System.out.println("time: " + (System.currentTimeMillis() - start));
    }
}

আমার কম্পিউটারে লুপের জন্য নেস্টেড চালাতে প্রায় 4s সময় লাগে এবং কেন এত দিন লাগল তা আমি বুঝতে পারি না। বাহ্যিক লুপটি ১,০০,০০০ বার চলবে, লুপের অভ্যন্তরীণটি 1 বার চলবে (কারণ হ্যাশসেটের কোনও মান কখনই -1 হবে না) এবং হ্যাশসেট থেকে কোনও আইটেম সরানো ও (1) হয়, সুতরাং প্রায় 200,000 ক্রিয়াকলাপ হওয়া উচিত। যদি এক সেকেন্ডে সাধারণত 100,000,000 অপারেশন হয় তবে আমার কোডটি চালাতে 4s কীভাবে আসবে?

অতিরিক্তভাবে, যদি লাইনটি hashSet.remove(i);মন্তব্য করা হয়, কোডটি কেবল 16 মিমি নেয়। লুপের জন্য অভ্যন্তরটি যদি মন্তব্য করা হয় (তবে তা নয় hashSet.remove(i);), কোডটি কেবলমাত্র 8 এসএম লাগে।


4
আমি আপনার অনুসন্ধানের নিশ্চিত। আমি কারণ সম্পর্কে অনুমান করতে পারি, তবে আশা করি কোনও চতুর আকর্ষণীয় ব্যাখ্যা পোস্ট করবেন।
খেলাউড

1
দেখে মনে হচ্ছে for valলুপটি সময় গ্রহণ করার জিনিস। removeএখনও খুব দ্রুত। সেটটি সংশোধন করার পরে কোনও ধরণের ওভারহেড নতুন আইট্রেটর স্থাপন করছে ...?
খেলাউড

@apangin এ সম্পর্কে একটি ভাল ব্যাখ্যা সরবরাহ করেছে stackoverflow.com/a/59522575/108326 কেন জন্য for valলুপ ধীর। তবে লক্ষ করুন যে লুপটি মোটেই প্রয়োজন হয় না। আপনি যদি সেটটিতে -1 থেকে কোনও মান আলাদা আছে কিনা তা পরীক্ষা করতে চান তবে এটি পরীক্ষা করা আরও কার্যকর হবে hashSet.size() > 1 || !hashSet.contains(-1)
মার্কাস্ক

উত্তর:


32

আপনি একটি প্রান্তিক ব্যবহারের ক্ষেত্রে তৈরি করেছেন HashSet, যেখানে অ্যালগোরিদম চতুর্ভুজ জটিলতায় অবনমিত হয়।

সরলিকৃত লুপটি এখানে এত দীর্ঘ সময় নেয়:

for (int i = 0; i < 100_000; i++) {
    hashSet.iterator().next();
    hashSet.remove(i);
}

অ্যাসিঙ্ক-প্রোফাইলার দেখায় যে প্রায় সমস্ত সময় java.util.HashMap$HashIterator()কনস্ট্রাক্টরের অভ্যন্তরে ব্যয় করা হয় :

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
--->        do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

হাইলাইট করা লাইনটি একটি লিনিয়ার লুপ যা হ্যাশ টেবিলের প্রথম খালি বালতি অনুসন্ধান করে।

যেহেতু Integerতুচ্ছ hashCode(যেমন হ্যাশকোড সংখ্যার সমান), তাই দেখা যাচ্ছে যে পরপর পূর্ণসংখ্যার বেশিরভাগটি হ্যাশ টেবিলের মধ্যে পরপর বালতিগুলি দখল করে: সংখ্যা 0 প্রথম বালতিতে যায়, 1 নম্বর দ্বিতীয় বালতিতে যায় ইত্যাদি etc.

এখন আপনি ধারাবাহিক সংখ্যা 0 থেকে 99999 থেকে মুছে ফেলুন the সবচেয়ে সহজ ক্ষেত্রে (যখন বালতিতে একটি কী থাকে) বালতি অ্যারেতে সংশ্লিষ্ট উপাদানটিকে বাতিল করে দেওয়ার জন্য একটি কী অপসারণ কার্যকর করা হয়। নোট করুন যে সারণীটি অপসারণের পরে কমপ্যাক্ট করা বা পুনঃনির্মাণ করা হয়নি।

সুতরাং, বালতি অ্যারের শুরু থেকে আপনি যত বেশি কী সরিয়ে ফেলবেন, HashIteratorপ্রথম খালি বালতিটি আর খুঁজে পাওয়ার দরকার নেই ।

অন্য প্রান্ত থেকে কীগুলি সরানোর চেষ্টা করুন:

hashSet.remove(100_000 - i);

অ্যালগরিদম নাটকীয়ভাবে দ্রুত হয়ে উঠবে!


1
আহ, আমি এটি পেরেছি কিন্তু প্রথম কয়েক রান করার পরে এটি বরখাস্ত করেছি এবং ভেবেছিলাম এটি সম্ভবত কিছু জেআইটি অপ্টিমাইজেশন হতে পারে এবং জেআইটিওয়াচের মাধ্যমে বিশ্লেষণে চলে এসেছিল। প্রথমে অ্যাসিঙ্ক-প্রোফাইলার চালানো উচিত ছিল। অভিশাপ!
অদ্বৈত কুমার

1
খুব আকর্ষণীয়। আপনি লুপের মধ্যে নিচের মত কিছু করতে হলে, এটা দ্রুত সম্পন্ন করা ভেতরের মানচিত্রের আকার কমিয়ে: if (i % 800 == 0) { hashSet = new HashSet<>(hashSet); }
ধূসর - সুতরাং
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.