2017-05-27 16 views
74

प्रस्तुति Core.dll में .NET Framework में, एक सामान्य PriorityQueue<T> वर्ग है जिसका कोड here पाया जा सकता है।माइक्रोसॉफ्ट के आंतरिक प्राथमिकता Queue <T> में बग?

मैं छंटाई परीक्षण करने के लिए एक छोटी कार्यक्रम में लिखा था, और परिणाम महान नहीं थे:

using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using MS.Internal; 

namespace ConsoleTest { 
    public static class ConsoleTest { 
     public static void Main() { 
      PriorityQueue<int> values = new PriorityQueue<int>(6, Comparer<int>.Default); 
      Random random = new Random(88); 
      for (int i = 0; i < 6; i++) 
       values.Push(random.Next(0, 10000000)); 
      int lastValue = int.MinValue; 
      int temp; 
      while (values.Count != 0) { 
       temp = values.Top; 
       values.Pop(); 
       if (temp >= lastValue) 
        lastValue = temp; 
       else 
        Console.WriteLine("found sorting error"); 
       Console.WriteLine(temp); 
      } 
      Console.ReadLine(); 
     } 
    } 
} 

परिणाम:

2789658 
3411390 
4618917 
6996709 
found sorting error 
6381637 
9367782 

एक छँटाई त्रुटि हुई है, और नमूने का आकार है अगर बढ़ी, सॉर्टिंग त्रुटियों की संख्या कुछ हद तक आनुपातिक रूप से बढ़ जाती है।

क्या मैंने कुछ गलत किया है? यदि नहीं, PriorityQueue कक्षा के कोड में बग कहां स्थित है?

+3

स्रोत कोड में टिप्पणियों के मुताबिक, माइक्रोसॉफ्ट 2005-02-14 के बाद से इस कोड का उपयोग कर रहा है। मुझे आश्चर्य है कि इस तरह की एक बग 12 साल से अधिक के लिए नोटिस से कैसे बच निकला? – Nat

+9

@Nat क्योंकि एकमात्र स्थान माइक्रोसॉफ्ट इसका उपयोग करता है [यहां है] (https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/MS/Internal/FontFace/PhysicalFontFamily.cs,185) और निचला चयन करने वाला फ़ॉन्ट प्राथमिकता टाइपफेस कुछ समय नोटिस करने के लिए एक कठिन बग है। –

उत्तर

75

व्यवहार प्रारंभिक वेक्टर [0, 1, 2, 4, 5, 3] का उपयोग करके पुन: उत्पन्न किया जा सकता है। परिणाम है:

[0, 1, 2, 4, 3, 5]

(हम देख सकते हैं कि 3 गलत तरीके से रखा जाता है)

Push एल्गोरिथ्म सही है।

  • अन्यथा नीचे दाईं
  • यदि मान अधिक है की तुलना में माता पिता के नोड तो डालें और वापसी से

    • प्रारंभ, में बजाय माता पिता डाल: यह एक सरल तरीके से एक मिनट-ढेर बनाता है नीचे सही स्थिति है, तो माता-पिता के स्थान पर मूल्य डालने की कोशिश (और जब तक सही जगह मिल गई है पेड़ गमागमन रखने)

    जिसके परिणामस्वरूप पेड़ है:

        0 
          / \ 
          / \ 
          1  2 
         /\ /
          4 5 3 
    

    समस्या Pop विधि के साथ है। (इस मामले में: 1)

        * 
          / \ 
          / \ 
          1  2 
         /\ /
          4 5 3 
    

    इसे भरने के लिए, यह सबसे कम तत्काल बच्चे की खोज करता है: यह एक "अंतर" को भरने के लिए के रूप में शीर्ष नोड पर विचार करके शुरू होता है (के बाद से हम इसे पॉप)। यह तो मान की खाई को भरने के लिए ले जाता है (और बच्चे अब नए खाई है):

        1 
          / \ 
          / \ 
          *  2 
         /\ /
          4 5 3 
    

    यह तो नई अंतराल के साथ सटीक एक ही बात करता है, तो अंतर को फिर से नीचे ले जाता है:

        1 
          / \ 
          / \ 
          4  2 
         /\ /
          * 5 3 
    

    जब खाई नीचे पहुँच गया है, एल्गोरिथ्म ... पेड़ के नीचे सबसे दायां मूल्य लेता है और यह का उपयोग करता है की खाई को भरने के लिए: अब

        1 
          / \ 
          / \ 
          4  2 
         /\ /
          3 5 * 
    

    उस खाई को bottom- पर है सही नोड, यह 0 घटता हैपेड़ से खाई को दूर करने के:

        1 
          / \ 
          / \ 
          4  2 
         /\  
          3 5 
    

    और हम साथ ... एक टूटे हुए ढेर खत्म।

    पूरी तरह ईमानदार होने के लिए, मुझे समझ में नहीं आता कि लेखक क्या करने का प्रयास कर रहा था, इसलिए मैं मौजूदा कोड को ठीक नहीं कर सकता। अधिक से अधिक, मैं इसे एक काम संस्करण (बेशर्म Wikipedia से नकल) के साथ स्वैप कर सकते हैं:

    internal void Pop2() 
    { 
        if (_count > 0) 
        { 
         _count--; 
         _heap[0] = _heap[_count]; 
    
         Heapify(0); 
        } 
    } 
    
    internal void Heapify(int i) 
    { 
        int left = (2 * i) + 1; 
        int right = left + 1; 
        int smallest = i; 
    
        if (left <= _count && _comparer.Compare(_heap[left], _heap[smallest]) < 0) 
        { 
         smallest = left; 
        } 
    
        if (right <= _count && _comparer.Compare(_heap[right], _heap[smallest]) < 0) 
        { 
         smallest = right; 
        } 
    
        if (smallest != i) 
        { 
         var pivot = _heap[i]; 
         _heap[i] = _heap[smallest]; 
         _heap[smallest] = pivot; 
    
         Heapify(smallest); 
        } 
    } 
    

    कि कोड के साथ मुख्य मुद्दा पुनरावर्ती कार्यान्वयन है, जो अगर तत्वों की संख्या बहुत बड़ी है टूट जाएगा है। मैं दृढ़ता से एक अनुकूलित तृतीय पक्ष पुस्तकालय का उपयोग करने की सलाह देते हैं।


    संपादित करें: मुझे लगता है कि मुझे पता चला कि क्या गुम है।

    internal void Pop() 
    { 
        Debug.Assert(_count != 0); 
    
        if (_count > 1) 
        { 
         // Loop invariants: 
         // 
         // 1. parent is the index of a gap in the logical tree 
         // 2. leftChild is 
         //  (a) the index of parent's left child if it has one, or 
         //  (b) a value >= _count if parent is a leaf node 
         // 
         int parent = 0; 
         int leftChild = HeapLeftChild(parent); 
    
         while (leftChild < _count) 
         { 
          int rightChild = HeapRightFromLeft(leftChild); 
          int bestChild = 
           (rightChild < _count && _comparer.Compare(_heap[rightChild], _heap[leftChild]) < 0) ? 
            rightChild : leftChild; 
    
          // Promote bestChild to fill the gap left by parent. 
          _heap[parent] = _heap[bestChild]; 
    
          // Restore invariants, i.e., let parent point to the gap. 
          parent = bestChild; 
          leftChild = HeapLeftChild(parent); 
         } 
    
         // Fill the last gap by moving the last (i.e., bottom-rightmost) node. 
         _heap[parent] = _heap[_count - 1]; 
    
         // FIX: Rebalance the heap 
         int index = parent; 
         var value = _heap[parent]; 
    
         while (index > 0) 
         { 
          int parentIndex = HeapParent(index); 
          if (_comparer.Compare(value, _heap[parentIndex]) < 0) 
          { 
           // value is a better match than the parent node so exchange 
           // places to preserve the "heap" property. 
           var pivot = _heap[index]; 
           _heap[index] = _heap[parentIndex]; 
           _heap[parentIndex] = pivot; 
           index = parentIndex; 
          } 
          else 
          { 
           // Heap is balanced 
           break; 
          } 
         } 
        } 
    
        _count--; 
    } 
    
  • +4

    'एल्गोरिदमिक त्रुटि' यह है कि आपको एक अंतर को नीचे नहीं ले जाना चाहिए, लेकिन पहले पेड़ को कम करें और उस अंतर में नीचे-दाएं तत्व डालें। फिर एक साधारण पुनरावृत्ति पाश में पेड़ की मरम्मत करें। –

    +4

    यह एक बग रिपोर्ट के लिए अच्छी सामग्री है, आपको इस पोस्ट के लिंक के साथ इसकी रिपोर्ट करनी चाहिए (मुझे लगता है कि सही स्थान [एमएस कनेक्ट] (http://connect.microsoft.com/) पर होगा क्योंकि प्रेजेंटेशनकोर गिटहब पर नहीं है)। –

    +4

    @LucasTrzesniewski मुझे वास्तविक दुनिया के अनुप्रयोग पर असर नहीं है (क्योंकि यह केवल WPF में कुछ अस्पष्ट फ़ॉन्ट-चयन कोड के लिए उपयोग किया जाता है), लेकिन मुझे लगता है कि यह रिपोर्ट करने के लिए कोई दिक्कत नहीं हो सकती है –

    16

    केविन Gosse के जवाब समस्या को दिखाता है: नीचे सबसे दायां नोड लेने के बाद, लेखक सिर्फ ढेर को संतुलित करने के लिए भूल गया था। यद्यपि ढेर का पुन: संतुलन काम करेगा, लेकिन यदि आप मूल हटाने लूप में मौलिक समस्या को ठीक करते हैं तो यह आवश्यक नहीं है।

    जैसा कि उन्होंने बताया, विचार है कि आइटम को सबसे कम, दाएं-सबसे अधिक आइटम के साथ ढेर के शीर्ष पर प्रतिस्थापित करना है, और फिर उसे उचित स्थान पर ले जाना है। यह मूल लूप का एक साधारण संशोधन है:

    internal void Pop() 
    { 
        Debug.Assert(_count != 0); 
    
        if (_count > 0) 
        { 
         --_count; 
         // Logically, we're moving the last item (lowest, right-most) 
         // to the root and then sifting it down. 
         int ix = 0; 
         while (ix < _count/2) 
         { 
          // find the smallest child 
          int smallestChild = HeapLeftChild(ix); 
          int rightChild = HeapRightFromLeft(smallestChild); 
          if (rightChild < _count-1 && _comparer.Compare(_heap[rightChild], _heap[smallestChild]) < 0) 
          { 
           smallestChild = rightChild; 
          } 
    
          // If the item is less than or equal to the smallest child item, 
          // then we're done. 
          if (_comparer.Compare(_heap[_count], _heap[smallestChild]) <= 0) 
          { 
           break; 
          } 
    
          // Otherwise, move the child up 
          _heap[ix] = _heap[smallestChild]; 
    
          // and adjust the index 
          ix = smallestChild; 
         } 
         // Place the item where it belongs 
         _heap[ix] = _heap[_count]; 
         // and clear the position it used to occupy 
         _heap[_count] = default(T); 
        } 
    } 
    

    ध्यान दें कि लिखित कोड में स्मृति रिसाव है। कोड का यह बिट:

     // Fill the last gap by moving the last (i.e., bottom-rightmost) node. 
         _heap[parent] = _heap[_count - 1]; 
    

    _heap[_count - 1] से मान को साफ़ नहीं करता है। यदि ढेर संदर्भ प्रकारों को संग्रहीत कर रहा है, तो संदर्भ ढेर में रहते हैं और कचरा इकट्ठा नहीं किया जा सकता है जब तक ढेर के लिए स्मृति कचरा इकट्ठा नहीं किया जाता है। मुझे नहीं पता कि यह ढेर कहाँ उपयोग किया जाता है, लेकिन यदि यह बड़ा है और किसी भी महत्वपूर्ण समय के लिए रहता है, तो इससे अतिरिक्त स्मृति खपत हो सकती है। जवाब कॉपी करने के बाद आइटम को साफ़ करना है:

    _heap[_count - 1] = default(T); 
    

    मेरा प्रतिस्थापन कोड उस फिक्स को शामिल करता है।

    +0

    एक बेंचमार्क में मैंने परीक्षण किया (pastebin.com/Hgkcq3ex पर पाया जा सकता है), यह संस्करण केविन गोस द्वारा प्रस्तावित एक से लगभग 18% धीमा है (भले ही डिफ़ॉल्ट() को स्पष्ट किया गया हो) और '_count/2' गणना बाहर हो गई हो सूचित करते रहना)। –

    +0

    @MathuSumMut: मैंने एक अनुकूलित संस्करण प्रदान किया। आइटम को रखने और लगातार इसे स्वैप करने के बजाय, मैं बस जगह के साथ आइटम की तुलना करता हूं। इससे लिखने की संख्या कम हो जाती है, इसलिए गति को बढ़ाया जाना चाहिए। एक और संभावित अनुकूलन '_heap [_count] 'को अस्थायी रूप से कॉपी करना होगा, जो सरणी संदर्भों की संख्या को कम करेगा। –

    +0

    दुर्भाग्य से मैंने इसे आजमाया और ऐसा लगता है कि इसमें एक बग भी है।टाइप int की कतार सेट करें, और इस कस्टम तुलनाकर्ता का उपयोग करें: 'तुलनाकर्ता । ((I1, i2) => -i1.compareTo (i2)) - अर्थात्, इसे कम से कम क्रमबद्ध करने के लिए (ऋणात्मक चिह्न नोट करें)। संख्याओं को क्रमशः धक्का देने के बाद: 3, 1, 5, 0, 4, और फिर उन सभी को हटाने के माध्यम से चलना, वापसी आदेश था: {5,4,1,3,0}, इसलिए अधिकांशतः क्रमबद्ध, लेकिन 1 और 3 गलत क्रम में हैं। ऊपर गोस्से की विधि का उपयोग करने में यह समस्या नहीं थी। ध्यान दें कि मुझे सामान्य, आरोही क्रम में यह समस्या नहीं थी। –

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