एक स्ट्रिंग आधारित कुंजी/मूल्य संग्रह तेजी से कैसे खोजें

हैलो साथी stackoverflowers!

मेरे पास 200,000 स्ट्रिंग प्रविष्टियों की एक शब्द सूची है, औसत स्ट्रिंग लंबाई लगभग 30 वर्ण है। शब्दों की यह सूची कुंजी है और प्रत्येक कुंजी के लिए मेरे पास डोमेन ऑब्जेक्ट है। मैं इस संग्रह में डोमेन ऑब्जेक्ट्स को केवल कुंजी का एक हिस्सा जानकर ढूंढना चाहता हूं। अर्थात। खोज स्ट्रिंग "कोव" उदाहरण के लिए कुंजी "stackoverflow" से मेल खाती है।

वर्तमान में मैं एक टर्नरी सर्च ट्री (टीएसटी) का उपयोग कर रहा हूं, जो आमतौर पर 100 मिलीसेकंड के भीतर आइटम पायेगा। हालांकि यह मेरी आवश्यकताओं के लिए बहुत धीमी है। कुछ मामूली अनुकूलन के साथ टीएसटी कार्यान्वयन में सुधार किया जा सकता है और मैं पेड़ को संतुलित करने की कोशिश कर सकता हूं। लेकिन मुझे लगा कि ये चीजें मुझे 5x-10x गति सुधार नहीं देगी जिसका लक्ष्य है। मुझे लगता है कि इतनी धीमी होने का कारण यह है कि मुझे मूल रूप से पेड़ में अधिकांश नोड्स का दौरा करना पड़ता है।

एल्गोरिदम की गति को सुधारने के तरीके पर कोई विचार? क्या कोई अन्य एल्गोरिदम है जिसे मुझे देखना चाहिए?

अग्रिम में धन्यवाद, ऑस्कर

13
आज एक नई बात सीख ली: ट्री।
जोड़ा लेखक Will, स्रोत
आप किस भाषा में काम कर रहे हैं? यह जानकारी आवश्यक है क्योंकि सभी भाषाएं खोजों और संग्रहों को संभालती नहीं हैं
जोड़ा लेखक WolfmanDragon, स्रोत
मुझे लगता है कि यह या तो "ट्री" या "टर्नरी सर्च ट्री" होना चाहिए।
जोड़ा लेखक Tomalak, स्रोत
यही वह प्रश्न है जो मुझे पसंद है: अब कुछ भी अच्छी चुनौती नहीं है और फिर ... :-)
जोड़ा लेखक Konrad Rudolph, स्रोत
ए। क्या आप समझा सकते हैं कि आपने टीएसटी का उपयोग कैसे किया है, जो किसी चीज की खोज के लिए प्रतीत होता है जो न तो उपसर्ग है और न ही प्रत्यय है? (आपके उदाहरण में, "कोव" न तो उपसर्ग है और न ही "स्टैक ओवरफ्लो" के लिए प्रत्यय है), यानी आप टीएसटी में सम्मिलित तत्वों के तरीके का वर्णन कर सकते हैं? बी। क्या आप कह सकते हैं, फिर से "कोव" के अपने विशिष्ट उदाहरण के लिए - वर्णन करें कि आपका टीएसटी खोज फ़ंक्शन कार्यान्वयन कैसे पता चलता है कि निरीक्षण से कुछ नोड्स को कैसे/कब बाहर निकालना है (फिर से ए से धारणा के तहत आप एक शब्द की खोज कर रहे हैं न तो उपसर्ग और न ही प्रत्यय)?
जोड़ा लेखक MrCC, स्रोत

7 उत्तर

प्रत्यय ऐरे और q -ग्राम अनुक्रमणिका

यदि आपके तारों के आकार पर सख्त ऊपरी सीमा है तो आप प्रत्यय सरणी </मजबूत> : केवल एक विशेष चरित्र (जैसे नल चार) का उपयोग करके अपने सभी तारों को उसी अधिकतम लंबाई तक पैड करें। फिर सभी तारों को संयोजित करें और उन पर एक प्रत्यय सरणी अनुक्रमणिका बनाएं।

This gives you a lookup runtime of m * log n where m is the length of your query string and n is the overall length of your combined strings. If this still isn't good enough and your m has a fixed, small length, and your alphabet Σ is restricted in size (say, Σ < 128 different characters) you can additionally build a q-gram index. This will allow retrieval in constant time. However, the q-gram table requires Σm entries (= 8 MiB in the case of just 3 characters, and 1 GiB for 4 characters!).

सूचकांक को छोटा बनाना

हैश फ़ंक्शन को समायोजित करके q -gram तालिका (घातीय रूप से, सर्वोत्तम मामले में) के आकार को कम करना संभव हो सकता है। प्रत्येक संभावित q -gram के लिए एक अद्वितीय संख्या असाइन करने के बजाय आप एक हानिकारक हैश फ़ंक्शन को नियोजित कर सकते हैं। तब तालिका को सटीक मिलान के अनुरूप केवल एक प्रत्यय सरणी प्रविष्टि के बजाय संभावित प्रत्यय सरणी सूचकांक की सूचियां संग्रहित करनी होंगी। यह होगा कि लुकअप अब स्थिर नहीं है, हालांकि, सूची में सभी प्रविष्टियों पर विचार करना होगा।

वैसे, मुझे यकीन नहीं है कि आप कैसे q -gram अनुक्रमणिका काम करता है से परिचित हैं क्योंकि इंटरनेट इस विषय पर सहायक नहीं है। मैंने पहले किसी अन्य विषय में इसका उल्लेख किया है। इसलिए मैंने अपने बैचलर थीसिस में निर्माण के लिए एक विवरण और एक एल्गोरिदम शामिल किया है।

अवधारणा के सुबूत

I've written a very small C# अवधारणा के सुबूत (since you stated otherwise that you worked with C#). It works, however it is very slow for two reasons. First, the suffix array creation simply sorts the suffixes. This alone has runtime n2 log n. There are far superior methods. Worse, however, is the fact that I use SubString to obtain the suffixes. Unfortunately, .NET creates copies of the whole suffix for this. To use this code in practice, make sure that you use in-place methods which do not copy any data around unnecessarily. The same is true for retrieving the q-grams from the string.

मेरे उदाहरण में उपयोग की जाने वाली m_Data </​​code> स्ट्रिंग का निर्माण करना संभवतः बेहतर होगा। इसके बजाय, आप मूल सरणी के संदर्भ को सहेज सकते हैं और इस सरणी पर काम करके मेरे सभी SubString accesss को अनुकरण कर सकते हैं।

फिर भी, यह देखना आसान है कि इस कार्यान्वयन ने अनिवार्य रूप से निरंतर समय पुनर्प्राप्ति की उम्मीद की है (यदि शब्दकोश अच्छी तरह से व्यवहार किया जाता है)! यह काफी उपलब्धि है जिसे खोज पेड़/ट्राई द्वारा संभवतः पीटा नहीं जा सकता है!

class QGramIndex {
    private readonly int m_Maxlen;
    private readonly string m_Data;
    private readonly int m_Q;
    private int[] m_SA;
    private Dictionary m_Dir = new Dictionary();

    private struct StrCmp : IComparer {
        public readonly String Data;
        public StrCmp(string data) { Data = data; }
        public int Compare(int x, int y) {
            return string.CompareOrdinal(Data.Substring(x), Data.Substring(y));
        }
    }

    private readonly StrCmp cmp;

    public QGramIndex(IList strings, int maxlen, int q) {
        m_Maxlen = maxlen;
        m_Q = q;

        var sb = new StringBuilder(strings.Count * maxlen);
        foreach (string str in strings)
            sb.AppendFormat(str.PadRight(maxlen, '\u0000'));
        m_Data = sb.ToString();
        cmp = new StrCmp(m_Data);
        MakeSuffixArray();
        MakeIndex();
    }

    public int this[string s] { get { return FindInIndex(s); } }

    private void MakeSuffixArray() {
       //Approx. runtime: n^3 * log n!!!
       //But I claim the shortest ever implementation of a suffix array!
        m_SA = Enumerable.Range(0, m_Data.Length).ToArray();
        Array.Sort(m_SA, cmp);
    }

    private int FindInArray(int ith) {
        return Array.BinarySearch(m_SA, ith, cmp);
    }

    private int FindInIndex(string s) {
        int idx;
        if (!m_Dir.TryGetValue(s, out idx))
            return -1;
        return m_SA[idx]/m_Maxlen;
    }

    private string QGram(int i) {
        return i > m_Data.Length - m_Q ?
            m_Data.Substring(i) :
            m_Data.Substring(i, m_Q);
    }

    private void MakeIndex() {
        for (int i = 0; i < m_Data.Length; ++i) {
            int pos = FindInArray(i);
            if (pos < 0) continue;
            m_Dir[QGram(i)] = pos;
        }
    }
}

उपयोग का उदाहरण:

static void Main(string[] args) {
    var strings = new [] { "hello", "world", "this", "is", "a",
                           "funny", "test", "which", "i", "have",
                           "taken", "much", "too", "far", "already" };

    var index = new QGramIndex(strings, 10, 3);

    var tests = new [] { "xyz", "aki", "ake", "muc", "uch", "too", "fun", "est",
                         "hic", "ell", "llo", "his" };

    foreach (var str in tests) {
        int pos = index[str];
        if (pos > -1)
            Console.WriteLine("\"{0}\" found in \"{1}\".", str, strings[pos]);
        else
            Console.WriteLine("\"{0}\" not found.", str);
    }
}
13
जोड़ा
क्या एक क्यू-ग्राम टेबल को विभाजित करने का कोई तरीका है ताकि आप इसका उपयोग कर डिस्क को थ्रैश न करें?
जोड़ा लेखक Will, स्रोत
मुझे किसी भी तरह से पता नहीं है। आपकी सबसे अच्छी शर्त एक ही कुंजी के कई पात्रों को हराकर वर्णमाला को कम कर सकती है, और इस प्रकार टेबल आकार को तेजी से कम कर सकती है। हालांकि, आपको टकराव की देखभाल करने की आवश्यकता है।
जोड़ा लेखक Konrad Rudolph, स्रोत
@ राफल: मैं तारों को पैडिंग कर रहा हूं इसलिए मैं इसके सूचकांक की गणना कर सकता हूं, आसानी से प्रत्यय सरणी में स्थिति बना सकता हूं। अन्य समाधान भी हैं लेकिन इन्हें प्रत्यय सरणी को संशोधित करने की आवश्यकता है, जिससे निर्माण कठिन हो जाता है।
जोड़ा लेखक Konrad Rudolph, स्रोत
एक प्रत्यय सरणी एक प्रत्यय पेड़ से बेहतर है क्योंकि इसे अधिक जगह-कुशलता से संग्रहीत किया जा सकता है। सबसे महत्वपूर्ण बात यह है कि क्यू-ग्राम इंडेक्स को कुशलतापूर्वक बनाने के लिए आपको एक प्रत्यय सरणी की आवश्यकता है (कम से कम मुझे प्रत्यय पेड़ के लिए क्यू-ग्राम इंडेक्स बनाने के लिए कोई एल्गोरिदम नहीं पता है)।
जोड़ा लेखक Konrad Rudolph, स्रोत
@ राफल: "प्रत्यय द्वारा मूल स्ट्रिंग को ढूंढना तेज़ होना चाहिए" - कैसे? हालांकि, मुझे लगता है कि स्ट्रिंग पैडिंग आमतौर पर एक अच्छा तरीका नहीं है। तारों की सरणी पर प्रत्यय सरणी बनाना बेहतर होगा। यह संभव है, यद्यपि थोड़ा कठिन है। मैं तदनुसार अपना पाठ अपडेट करूंगा।
जोड़ा लेखक Konrad Rudolph, स्रोत
@ राफल: मेरी फॉलो-अप पोस्ट पर एक नज़र डालें। हालांकि, आपके लॉग (एन) प्रस्ताव के जवाब में: ध्यान रखें कि आपका एन यहां केवल 200,000 नहीं है बल्कि इसके बजाय यह सभी प्रत्ययों की संख्या है, जो बहुत अधिक है।
जोड़ा लेखक Konrad Rudolph, स्रोत
तारों को पैडिंग क्यों जरूरी है? प्रत्यय पेड़ एक प्रत्यय पेड़ से बेहतर है?
जोड़ा लेखक Rafał Dowgird, स्रोत
पेड़ के बारे में अच्छे अंक। पैडिंग पर वापस - जैसा कि मैं समझता हूं, आप तालिका से पूरा प्रत्यय प्राप्त कर सकते हैं ("kov" -> "koverflow")। प्रत्यय द्वारा मूल स्ट्रिंग को ढूंढना तेज़ होना चाहिए (या उपसर्ग द्वारा भी, यदि आप उलटा तारों से तालिका बनाते हैं)। सही बात?
जोड़ा लेखक Rafał Dowgird, स्रोत
यदि आप अपने रिवर्स द्वारा क्रमबद्ध तारों की एक अतिरिक्त तालिका रखते हैं तो आप O (लॉग (एन)) समय में प्रत्यय द्वारा स्ट्रिंग पा सकते हैं। या तारों को स्वाभाविक रूप से क्रमबद्ध रखें और प्रत्यय के बजाय उपसर्ग प्राप्त करने के बाद, उलटा तारों से प्रत्यय सरणी बनाएं।
जोड़ा लेखक Rafał Dowgird, स्रोत

Here's a WAG for you. I am in NO WAY Knuthian in my algorithm savvy

Okay, so the naiive Trie encodes string keys by starting at the root of the tree and moving down branches that match each letter in the key, starting at the first letter of the key. So the key "foo" would be mapped to (root)->f->fo->foo and the value would be stored in the location pointed to by the 'foo' node.

आप कुंजी के भीतर किसी भी सबस्ट्रिंग की खोज कर रहे हैं, न केवल कुंजी की शुरुआत में शुरू होने वाले सबस्ट्रिंग्स।

तो, आपको जो करने की ज़रूरत है, वह किसी भी कुंजी के साथ नोड को जोड़ती है जिसमें उस विशेष सबस्ट्रिंग होती है। मैंने पहले दिए गए foo उदाहरण में, आपको noo 'f' और 'fo' के अंतर्गत foo के मान का संदर्भ नहीं मिला होगा। एक टीएसटी में जो खोजों के प्रकार का समर्थन करता है, आपको न केवल तीन ऑब्जेक्ट्स ('एफ', 'एफओ', और 'फू') के तहत foo ऑब्जेक्ट मिल जाएगा, आपको यह भी मिल जाएगा 'ओ' और 'ओओ' के तहत भी।

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

दूसरा, आपको यह पता चल जाएगा कि छोटे तारों की खोजों के परिणामस्वरूप बड़ी संख्या में हिट हो सकती हैं, जो आपकी खोज को बेकार बना सकती हैं जब तक कि आप कहें, खोज शब्दों पर न्यूनतम लंबाई दें।

On the bright side, you might also find that you can compress the tree via tokenization (like zip compression does) or by compressing nodes that don't branch down (i.e., if you have 'w'->'o'->'o'-> and the first 'o' doesn't branch, you can safely collapse it to 'w'->'oo'). Maybe even a wicked-ass hash could make things easier...

वैसे भी, जैसा कि मैंने कहा वैग।

2
जोड़ा
क्या यह क्यू-ग्राम इंडेक्स कोनाड के बारे में बात नहीं कर रहा था?
जोड़ा लेखक Pacerier, स्रोत

/संपादित करें: मेरे एक दोस्त ने क्यू-ग्राम तालिका के निर्माण में एक बेवकूफ धारणा की ओर इशारा किया। निर्माण को बहुत आसान बनाया जा सकता है - और इसके परिणामस्वरूप, बहुत तेज़। मैंने इसे प्रतिबिंबित करने के लिए स्रोत कोड और स्पष्टीकरण संपादित किया है। मुझे लगता है कि यह अंतिम समाधान हो सकता है।

मेरे पिछले जवाब में राफल डॉउगर्ड की टिप्पणी से प्रेरित, मैंने अपना कोड अपडेट कर लिया है। मुझे लगता है कि यह एक खुद का जवाब योग्यता है, क्योंकि यह भी काफी लंबा है। मौजूदा तारों को पैड करने के बजाय, यह कोड स्ट्रिंग की मूल सरणी पर इंडेक्स बनाता है। एक ही स्थिति को संग्रहीत करने के बजाय, प्रत्यय सरणी एक जोड़ी स्टोर करती है: लक्ष्य स्ट्रिंग का सूचकांक और उस स्ट्रिंग में प्रत्यय की स्थिति। नतीजतन, केवल पहले नंबर की आवश्यकता है। हालांकि, q -gram तालिका के निर्माण के लिए दूसरा नंबर आवश्यक है।

एल्गोरिदम का नया संस्करण मूल स्ट्रिंग्स के बजाय प्रत्यय सरणी पर चलकर q -gram तालिका बनाता है। यह प्रत्यय सरणी की बाइनरी खोज बचाता है। नतीजतन, निर्माण का रनटाइम ( n * लॉग n ) से नीचे O ( n ) (जहां n प्रत्यय सरणी का आकार है)।

ध्यान दें कि, मेरे पहले समाधान की तरह, SubString का उपयोग बहुत अनावश्यक प्रतियों में होता है। स्पष्ट समाधान एक विस्तार विधि लिखना है जो स्ट्रिंग की प्रतिलिपि बनाने के बजाय हल्के रैपर बनाता है। तुलना तब थोड़ी अनुकूलित होनी चाहिए। यह पाठक के लिए एक अभ्यास के रूप में छोड़ दिया गया है। ;-)

using Position = System.Collections.Generic.KeyValuePair;

class QGramIndex {
    private readonly int m_Q;
    private readonly IList m_Data;
    private Position[] m_SA;
    private Dictionary m_Dir;

    public QGramIndex(IList strings, int q) {
        m_Q = q;
        m_Data = strings;
        MakeSuffixArray();
        MakeIndex();
    }

    public int this[string s] { get { return FindInIndex(s); } }

    private int FindInIndex(string s) {
        int idx;
        if (!m_Dir.TryGetValue(s, out idx))
            return -1;
        return m_SA[idx].Key;
    }

    private void MakeSuffixArray() {
        int size = m_Data.Sum(str => str.Length < m_Q ? 0 : str.Length - m_Q + 1);
        m_SA = new Position[size];
        int pos = 0;
        for (int i = 0; i < m_Data.Count; ++i)
            for (int j = 0; j <= m_Data[i].Length - m_Q; ++j)
                m_SA[pos++] = new Position(i, j);

        Array.Sort(
            m_SA,
            (x, y) => string.CompareOrdinal(
                m_Data[x.Key].Substring(x.Value),
                m_Data[y.Key].Substring(y.Value)
            )
        );
    }

    private void MakeIndex() {
        m_Dir = new Dictionary(m_SA.Length);

       //Every q-gram is a prefix in the suffix table.
        for (int i = 0; i < m_SA.Length; ++i) {
            var pos = m_SA[i];
            m_Dir[m_Data[pos.Key].Substring(pos.Value, 5)] = i;
        }
    }
}

उपयोग अन्य उदाहरण के समान है, कन्स्ट्रक्टर के लिए आवश्यक maxlen तर्क को घटाएं।

0
जोड़ा

क्या आपको मशीन ट्रिप के आकार के मुकाबले आपकी ट्राई कुंजियों के साथ तुलना करने का कोई फायदा होगा? तो यदि आप 32 बिट बॉक्स पर हैं तो आप अलग-अलग चरित्रों की बजाय एक बार में 4 वर्णों की तुलना कर सकते हैं? मुझे नहीं पता कि आपके ऐप के आकार में कितना बुरा होगा।

0
जोड़ा

क्या मुख्य मूल्य "हैश" संभव होगा? मूल रूप से एक दूसरा पेड़ होगा जो सभी संभव मूल्यों को पहली पेड़ में कुंजियों की सूची को इंगित करने के लिए खोज करेगा।

आपको 2 पेड़ों की आवश्यकता होगी; पहला डोमेन डोमेन ऑब्जेक्ट का हैश मान है। दूसरा पेड़ हैश मूल्य के लिए खोज तार है। दूसरे पेड़ में एक ही हैश मान के लिए कई कुंजी हैं।

Example tree 1: STCKVRFLW -> domain object

tree 2: stack -> STCKVRFLW,STCK over -> STCKVRFLW, VRBRD, VR

तो दूसरे पेड़ पर खोज का उपयोग करके आपको पहले पेड़ पर खोजने के लिए चाबियों की एक सूची मिलती है।

0
जोड़ा

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

इसका सबसे बुरा मामला ओ (एन) है, लेकिन आप केवल तभी प्राप्त करेंगे यदि आपकी स्ट्रिंग प्रविष्टियां लगभग समान हैं। लुकअप डिक्शनरी काफी बड़ा होने की संभावना है, इसलिए डिस्क पर इसे स्टोर करना या रिलेशनल डेटाबेस का उपयोग करना शायद एक अच्छा विचार है :-)

0
जोड़ा

टेक्स्ट के बड़े सेट को कुशल तरीके से पूछने के लिए आप संपादन दूरी/उपसर्ग संपादन दूरी की अवधारणा का उपयोग कर सकते हैं।

दूरी ईडी (एक्स, वाई) संपादित करें: एक्स से वाई

प्राप्त करने के लिए ट्रांसफॉर्म की न्यूनतम संख्या

लेकिन प्रत्येक शब्द और क्वेरी पाठ के बीच ईडी कंप्यूटिंग संसाधन और समय लेने वाला है। इसलिए प्रत्येक शब्द के लिए ईडी की गणना करने के बजाय हम Qgram Index नामक तकनीक का उपयोग करके संभावित मिलान शर्तों को निकाल सकते हैं। और फिर उन चयनित शर्तों पर ईडी गणना लागू करें।

Qgram अनुक्रमणिका तकनीक का एक लाभ यह फ़ज़ी खोज के लिए समर्थन करता है।

क्यूजीआरएम इंडेक्स को अनुकूलित करने का एक संभावित दृष्टिकोण क्यूग्राम का उपयोग करके एक उलटा इंडेक्स बनाना है। वहां हम उन सभी शब्दों को संग्रहीत करते हैं जिनमें विशेष क्यूग्राम (पूर्ण स्ट्रिंग को संग्रहीत करने के बजाय आप प्रत्येक स्ट्रिंग के लिए अद्वितीय आईडी का उपयोग कर सकते हैं) के साथ होते हैं।

col: col mbia, col ombo, gan col a, ta col ama

फिर पूछताछ करते समय, हम क्वेरी टेक्स्ट और उपलब्ध शर्तों के बीच सामान्य क्यूग्राम की संख्या की गणना करते हैं।

Example: x = HILLARY, y = HILARI(query term)
Qgrams
$$HILLARY$$ -> $$H, $HI, HIL, ILL, LLA, LAR, ARY, RY$, Y$$
$$HILARI$$ -> $$H, $HI, HIL, ILA, LAR, ARI, RI$, I$$
number of q-grams in common = 4

सामान्य क्यूग्राम की उच्च संख्या वाले शब्दों के लिए, हम क्वेरी शब्द के विरुद्ध ईडी/पीईडी की गणना करते हैं और फिर अंतिम उपयोगकर्ता को शब्द का सुझाव देते हैं।

you can find an implementation of this theory in following project. Feel free to ask any questions. https://github.com/Bhashitha-Gamage/City_Search

संपादन दूरी के बारे में अधिक अध्ययन करने के लिए, उपसर्ग संपादित करें क्यूग्रम इंडेक्स कृपया प्रोफेसर डॉ हन्ना बस्ट के निम्नलिखित वीडियो देखें https://www.youtube.com/embed/6pUg2wmGJRo (पाठ 20:06 से शुरू होता है )

0
जोड़ा