2015-05-16 13 views
12

पृष्ठभूमि: मैंने पहली बार कई वर्षों पहले स्कूल में सी ++ और जावा सीखा था, लेकिन पिछले 9 या उससे अधिक वर्षों में मैंने अधिक प्रोग्रामिंग नहीं की है क्योंकि मेरे पिछले करियर की आवश्यकता नहीं थी।प्रोजेक्ट यूलर # 14: ब्रूट फोर्स की तुलना में मेरा ट्रीमैप एल्गोरिदम धीमा क्यों है?

मैंने अपने प्रोग्रामिंग पर ब्रश करने के लिए प्रोजेक्ट यूलर को देखने का फैसला किया और समस्या हल की 14, जो लंबे समय तक कोलात्ज़ अनुक्रम के साथ एक और एक मिलियन के बीच पूर्णांक खोजने के लिए कहता है। (कोलात्ज़ अनुक्रम, प्रारंभिक संख्या के साथ आगे बढ़ता है, 3 से संख्या को गुणा करता है और यदि यह अजीब है, या यदि यह भी है तो संख्या को जोड़ना। प्रक्रिया तब तक जारी रहती है जब तक कि संख्या तक पहुंच न हो।)

मैंने पहले हल किया ब्रूट फोर्स का उपयोग कर समस्या, जैसा कि नीचे दिए गए कोड में दिखाया गया है।

int n; 
long temp; // long is necessary since some Collatz sequences go outside scope of int 
int[] n_length = new int[1000000]; 
    for(n = 0; n < 1000000; n++){ 
     temp = n + 1; 
     n_length[n] = 1; 
     while (temp > 1){ 
      n_length[n]++; 
      if (temp % 2 == 0) temp = temp/2; 
      else temp = 3*temp + 1; 

     } 
    } 
int max = 0; 
    int max_index = 0; 
    for (int i = 0; i < 1000000; i++){ 
     if (n_length[i] > max){ 
      max = n_length[i]; 
      max_index = i; 
     } 
    } 
    System.out.println("The number with the longest Collatz sequence is " + (max_index + 1)); 

मैंने सोचा कि यह दृष्टिकोण अक्षम होगा, क्योंकि यह आवश्यक से अधिक बार एल्गोरिदम चलाता है। किसी भी संख्या जो पिछले नंबर के कोलात्ज़ अनुक्रम का हिस्सा है, प्रभावी रूप से इसके अनुक्रम को पहले ही निर्धारित कर लेगा, और इसलिए आप प्रत्येक एकल संख्या के अनुक्रम की गणना को प्रत्येक बार एक कोलेट्स अनुक्रम में प्राप्त करते हैं।

मैंने तय किया कि जैसे ही यह कोलात्ज़ अनुक्रम में आता है, प्रत्येक मानचित्र को मानचित्र में स्टोर करना बेहतर होगा, इसलिए आपको केवल इसकी गणना करना होगा। ऐसा करने के लिए, मैंने एक वृक्ष मैप का उपयोग किया, जिसमें कुंजियों और सहयोगी कोलात्ज़ अनुक्रम लंबाई के रूप में उपयोग की जाने वाली संख्याओं के साथ, और प्रत्येक संख्या को कोलात्ज़ अनुक्रम में आने पर मानचित्र में प्रत्येक नंबर डालने के लिए एक पुनरावर्ती फ़ंक्शन का उपयोग किया जाता था। इतनी के रूप में मैं पाश के लिए के माध्यम से पुनरावृति मैं जल्दी से देख सकते हैं कि कुंजियां पहले से ही डाला गया है और फोन करने से बचने के बाद से यह स्वचालित रूप से प्रवेश पर कुंजी सॉर्ट करता (कोड नीचे देखें।)

public static TreeMap<Long, Integer> tm = new TreeMap<Long, Integer>(); 
public static void main(String[] args) { 

    tm.put((long)1, 1); 
    int maxVal = 1; 
    long keyWithMaxVal = 1; 
    int maybeMax; 
    for (long i = 2; i <= 1000000; i++){ 
     if(!(tm.containsKey(i))){ 
      maybeMax = addKey(i); 
      if (maybeMax >= maxVal){ 
       maxVal = maybeMax; 
       keyWithMaxVal = i; 
      } 
     } 
    } 
    System.out.println("The number with the longest Collatz sequence is " + keyWithMaxVal + " with length " + maxVal); 
} 
public static int addKey(long key){ 

    while (!(tm.containsKey(key))){ 
     if (key % 2 == 0){ 
      tm.put(key, 1 +addKey(key/2)); 
     } 
     else{ 
      tm.put(key, 1 + addKey(3*key + 1)); 
     } 
    } 
    return tm.get(key); 
} 

मैं एक ट्री-मैप का इस्तेमाल किया, जब तक मुझे नहीं करना है तब तक कुंजी जोड़ने के लिए addKey विधि। मैंने सोचा कि यह एल्गोरिदम बहुत तेज होगा।

हालांकि, जब मैं वास्तव में कोड चलाता था, तो मुझे आश्चर्य हुआ कि ब्रूट फोर्स एल्गोरिदम तत्काल उत्तर के साथ आया था, जबकि रिकर्सिव ट्रीएप एल्गोरिदम ने लगभग 6 सेकंड तक अधिक समय लगाया था। जब मैंने अपने कार्यक्रमों को एक मिलियन की बजाय 5 मिलियन तक जाने के लिए संशोधित किया, तो अंतर और भी स्पष्ट हो गया। मैंने यह सुनिश्चित करने के लिए प्रत्येक प्रोग्राम में कुछ कोड जोड़ा है कि दूसरा प्रोग्राम पहले से कम काम कर रहा था, और वास्तव में मैंने यह निर्धारित किया कि प्रत्येक कुंजी के लिए केवल ऐडकी विधि को बुलाया जा रहा था, जबकि समय लूप को फिर से करने की आवश्यकता थी पहले कार्यक्रम में सभी संख्याओं की लंबाई के योग के बराबर था Collatz अनुक्रम (यानी दूसरी एल्गोरिदम में विधि कॉल की संख्या से अधिक अक्सर)।

तो पहला एल्गोरिदम दूसरे की तुलना में इतना तेज़ क्यों है? क्या ऐसा इसलिए है क्योंकि पहले एल्गोरिदम में प्राइमेटिव की सरणी को दूसरे में रैपर ऑब्जेक्ट्स के ट्रीमैप की तुलना में कम संसाधनों की आवश्यकता होती है? क्या नक्शा खोज रहा है यह जांचने के लिए कि क्या एक कुंजी पहले से ही धीमी है (अनुमानित लॉग नहीं होना चाहिए?)? क्या रिकर्सिव विधियां हैं जिनकी बड़ी संख्या में विधि कॉल धीमी गति से धीमी होती है? या क्या कुछ और है जो मैं देख रहा हूं

+0

* क्या रिकर्सिव विधियां हैं जिनकी बड़ी संख्या में विधि कॉल धीमी गति से धीमी होती है? * आम तौर पर: हाँ। रिकर्सन अक्सर लिखना और समझना आसान होता है, लेकिन उन्हें लूप में बदलने से प्रदर्शन में काफी वृद्धि हो सकती है। लेकिन मुझे नहीं लगता कि यह निष्पादन समय में वृद्धि के लिए पूर्ण कारण है। – Turing85

+1

हालांकि यह समस्या गतिशील प्रोग्रामिंग (आपका दूसरा समाधान) का उपयोग करके सबसे अच्छी तरह हल हो जाती है, ऐसा लगता है कि आपका रिकर्सिव समाधान ठीक वही नहीं कर रहा है जो आप चाहते हैं। हैश मानचित्र के बजाय पेड़ मानचित्र का उपयोग क्यों करें? आप लॉग समय के बजाय अपने मानचित्र में निरंतर समय लुकअप प्राप्त कर सकते हैं। –

+0

हम्म ... हैश मैप पर स्विच करने से थोड़ा सुधार हुआ, लेकिन यह अभी भी पहले कार्यक्रम की तुलना में काफी धीमा है। (मैं अभ्यास से थोड़ी दूर हूं, जैसा कि मैंने अपने प्रश्न के शीर्ष पर समझाया है, इसलिए मुझे कुछ विवरण याद नहीं हो सकते हैं जैसे हैश मैप निरंतर लुकअप समय है।) – nobody

उत्तर

2

मैंने आपके कोड में कुछ बदलाव किए हैं, और यह तेज़ लगता है, हालांकि अभी भी तात्कालिक नहीं है।

आम तौर पर, मैंने अनावश्यक, बार-बार मानचित्र पहुंच से छुटकारा पाने की कोशिश की।

हैश मैप के साथ ट्रीएप को बदलना कुछ ओ (लॉग एन) ऑपरेशंस को ओ (1) में बदल देता है। आप वास्तव में ट्रीमैप की क्रमबद्ध संपत्ति का उपयोग नहीं करते हैं, बस इसकी विधि शामिल है।

मुख्य पाश में पीछे की तरफ maybeMax >= maxVal स्थिति की संख्या कम हो जाती है।

import java.util.HashMap; 

public class Test { 
    public static HashMap<Long, Integer> tm = new HashMap<Long, Integer>(); 

    public static void main(String[] args) { 
    tm.put((long) 1, 1); 
    int maxVal = 1; 
    long keyWithMaxVal = 1; 
    int maybeMax; 
    for (long i = 1000000; i >= 2; i--) { 
     if (!(tm.containsKey(i))) { 
     maybeMax = addKey(i); 
     if (maybeMax >= maxVal) { 
      maxVal = maybeMax; 
      keyWithMaxVal = i; 
     } 
     } 
    } 
    System.out.println("The number with the longest Collatz sequence is " 
     + keyWithMaxVal + " with length " + maxVal); 
    } 

    public static int addKey(long key) { 
    Integer boxedValue = tm.get(key); 
    if (boxedValue == null) { 
     if (key % 2 == 0) { 
     int value = 1 + addKey(key/2); 
     tm.put(key, value); 
     return value; 
     } else { 
     int value = 1 + addKey(3 * key + 1); 
     tm.put(key, value); 
     return value; 
     } 
    } 
    return boxedValue.intValue(); 
    } 
} 
2

मुझे लगता है कि ऑटो (अन) मुक्केबाजी समस्या का स्रोत है। यहां तक ​​कि Java SE 8 Programming Guide का उल्लेख है:

परिणामस्वरूप सूची का प्रदर्शन खराब होने की संभावना है, क्योंकि यह प्रत्येक प्राप्त या सेट ऑपरेशन पर बॉक्स या अनबॉक्स करता है। यह कभी-कभी उपयोग के लिए पर्याप्त तेज़ है, लेकिन यह महत्वपूर्ण आंतरिक लूप प्रदर्शन में इसका उपयोग करने के लिए मूर्खतापूर्ण होगा।

1

एच, मुझे लगता है कि उस परिणाम के लिए कंटेनकी जिम्मेदार है।

ट्री-मैप ContainsKey हे (लॉग (एन))

https://github.com/benblack86/java-snippets/blob/master/resources/java_collections.pdf

और http://en.wikipedia.org/wiki/Collatz_conjecture के अनुसार है:

किसी भी प्रारंभिक प्रारंभिक संख्या 100 लाख से भी कम समय 63,728,127 है के लिए सबसे लंबे समय तक प्रगति, जिसमें 9 4 9 कदम हैं।

हम सी के रूप में Collatz जटिलता में सोचेंगे

तो, अपने पहले मामले में तुम हो:

O (n * सी + एन) = O (n * (सी + 1)) = हे (k * एन)

और पुनरावर्ती समाधान में:

O (n * (लॉग (एन) + सी * लॉग (एन))) = हे (k * n * लॉग (एन))

(मैं सुंदर नहीं हूँ रिकर्सिव भाग के बारे में निश्चित है, लेकिन मुझे यकीन है कि 1 से अधिक है क्योंकि रिकर्सिव फ़ंक्शन के अंदर आप फिर से कॉल कर रहे हैं)

3

अन्य उत्तरों में पहले से उल्लिखित कारणों के अतिरिक्त, सरणी आधारित कार्यान्वयन का प्राथमिक कारण इसलिए बहुत तेजी से होने की वजह से यह सीपीयू कैशिंग प्रभाव के लाभ का एक बहुत कुछ होने की संभावना है:

  1. आपका दो अलग-अलग छोटे, तंग छोरों होगा पूरी तरह से एक आधुनिक CPU के L0 अनुदेश कैश में फिट (इसमें एक सैंडी ब्रिज पर 1,536 डीकोडेड माइक्रो ऑप्स शामिल हो सकते हैं)। अनुक्रमिक रूप से उन दोनों को चलाने से अधिक निर्देशों के साथ एक लूप की तुलना में बहुत तेज़ होने वाला है, जो उस कैश में फिट नहीं होता है। यह देखते हुए कि दूसरा पाश बहुत छोटा है, इसकी संभावना है कि इसके निर्देश पहले से ही माइक्रो ऑप्स के रूप में पहले से तय किए गए हैं और डीकोड किए गए हैं, और लूप ब्लॉक बफर (28 माइक्रो ऑप्स) में फिट होंगे।

    loop block buffer स्रोत: hardwaresecrets.com

  2. एक महान डेटा का उपयोग करने के संबंध में संदर्भ के इलाके है। आपके पहले और आपके दूसरे लूप दोनों में, जहां आप अनुक्रमिक पहुंच करते हैं। प्रीफ़ेचर भी मदद करता है, क्योंकि आपका एक्सेस पैटर्न पूरी तरह अनुमानित है।


इन दो विषयों और अधिक करने के लिए संबंधित है, मैं तुम्हें इस उत्कृष्ट "कास्ट कौशल" देखने की सलाह देता चाहते हैं: Martin Thompson द्वारा 95% of performance is about clean representative models, और अधिक विस्तार में इन और अन्य विषयों पर चर्चा करता है।

3

यह कोड परीक्षण करता है कि 1 और 5 मिलियन के बीच की संख्या के लिए सबसे लंबा कॉलैट अनुक्रम खोजने में कितना समय लगता है। यह तीन अलग-अलग तरीकों का उपयोग करता है: एक हैश मानचित्र में परिणामस्वरूप, पुनरावर्ती और भंडारण परिणाम।

उत्पादन चरणों कार्यक्रम लेने के लिए है की संख्या हैश नक्शा समाधान के लिए तो यह

iterative 
time = 2013ms 
max n: 3732423, length: 597 
number of iterations: 745438133 

recursive 
time = 2184ms 
max n: 3732423, length: 597 
number of iterations: 745438133 

with hash map 
time = 7463ms 
max n: 3732423, length: 597 
number of iterations: 15865083 

तरह लग रहा है लगभग 50 गुना छोटा होता है। इसके बावजूद यह 3 गुना धीमा है और मुझे लगता है कि इसका मुख्य कारण यह तथ्य है कि संख्याओं पर सरल गणितीय परिचालन उदा। जोड़ना, गुणा करना इत्यादि हैश मैप्स पर संचालन से बहुत तेज है।

import java.util.function.LongUnaryOperator; 
import java.util.HashMap; 

public class Collatz { 
    static int iterations = 0; 
    static HashMap<Long, Long> map = new HashMap<>(); 

    static long nextColl(long n) { 
    if(n % 2 == 0) return n/2; 
    else return n * 3 + 1; 
    } 

    static long collatzLength(long n) { 
    iterations++; 
    int length = 1; 
    while(n > 1) { 
     iterations++; 
     n = nextColl(n); 
     length++; 
    } 
    return length; 
    } 

    static long collatzLengthMap(long n) { 
    iterations++; 
    if(n == 1) return 1; 
    else return map.computeIfAbsent(n, x -> collatzLengthMap(nextColl(x)) + 1); 
    } 

    static long collatzLengthRec(long n) { 
    iterations++; 
    if(n == 1) return 1; 
    else return collatzLengthRec(nextColl(n)) + 1; 
    } 

    static void test(String msg, LongUnaryOperator f) { 
    iterations = 0; 
    long max = 0, maxN = 0; 
    long start = System.nanoTime(); 
    for(long i = 1; i <= 5000000; i++) { 
     long length = f.applyAsLong(i); 
     if(length > max) { 
     max = length; 
     maxN = i; 
     } 
    } 
    long end = System.nanoTime(); 
    System.out.println(msg); 
    System.out.println("time = " + ((end - start)/1000000) + "ms"); 
    System.out.println("max n: " + maxN + ", length: " + max); 
    System.out.println("number of iterations: " + iterations); 
    System.out.println(); 
    } 

    public static void main(String[] args) { 
    test("iterative", Collatz::collatzLength); 
    test("recursive", Collatz::collatzLengthRec); 
    test("with hash map", Collatz::collatzLengthMap); 
    } 
} 
+1

क्या आप 'हैश मैप' की प्रारंभिक क्षमता को उस तत्व की संख्या से 1.34 गुना अधिक सेट करने का प्रयास कर सकते हैं, जिसे आप धारण करते हैं? – jxh

2

के रूप में दूसरों के द्वारा कहा, आप बल्कि एक TreeMap का उपयोग कर, प्रविष्टि और पुनः प्राप्ति के संचालन की जटिलता को कम करने के लिए की तुलना में एक HashMap करने के लिए स्विच करना चाहिए।

हालांकि, HashMap का इष्टतम उपयोग इसकी प्रारंभिक क्षमता निर्धारित करने पर निर्भर करता है। यदि आप ऐसा नहीं करते हैं, तो आपके प्रविष्टि डिफ़ॉल्ट क्षमता से अधिक हो जाने पर, HashMap एक बड़ी तालिका को फिर से आवंटित कर देगा, और आपके आइटम नई तालिका में फिर से हो जाएंगे। यह आपके कार्यक्रम के निष्पादन को धीमा कर देगा।

कम से कम परिवर्तन होगा:

public static HashMap<Long, Integer> tm = new HashMap<Long, Integer>(1000000, 1.0); 

HashMap(int initialCapacity, float loadFactor)
एक खाली HashMap निर्दिष्ट आरंभिक क्षमता और लोड फैक्टर साथ निर्माण करती है।
Java documentation

यहाँ, हम राज्य हम HashMap चाहते का एक लोड फैक्टर के साथ (कि कई तत्वों पकड़ करने में सक्षम) 1000000 की क्षमता है 1.0 (सम्मिलन rehashing से पहले क्षमता के 100% से अधिक करने के लिए है जगह लेता है)।

संबंधित मुद्दे