बेंटले की कोडिंग चुनौती: k सबसे लगातार शब्द


18

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

tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed 10q

1987 में, एक प्रिंसटन प्रोफेसर द्वारा इस बार एक अन्य समाधान के साथ एक अनुवर्ती लेख प्रकाशित किया गया था। लेकिन यह भी एक भी बाइबिल के लिए परिणाम वापस नहीं कर सका!

समस्या का विवरण

मूल समस्या विवरण:

एक टेक्स्ट फ़ाइल और पूर्णांक k को देखते हुए, आप कम आवृत्ति में फ़ाइल (और उनकी घटनाओं की संख्या) में सबसे आम शब्दों को प्रिंट करने के लिए हैं।

अतिरिक्त समस्या स्पष्टीकरण:

  • नुथ ने एक शब्द को लैटिन अक्षरों की एक स्ट्रिंग के रूप में परिभाषित किया: [A-Za-z]+
  • अन्य सभी पात्रों को अनदेखा किया जाता है
  • अपरकेस और लोअरकेस अक्षर समतुल्य माने जाते हैं ( WoRd== word)
  • फ़ाइल आकार और न ही शब्द लंबाई पर कोई सीमा नहीं
  • लगातार शब्दों के बीच की दूरी मनमाने ढंग से बड़ी हो सकती है
  • सबसे तेज़ प्रोग्राम वह है जो कम से कम कुल CPU समय का उपयोग करता है (बहुविध रूप से मदद नहीं करेगा)

नमूना परीक्षण के मामले

टेस्ट 1: जेम्स जॉयस द्वारा यूलिस को 64 बार (96 एमबी फ़ाइल) संक्षिप्त किया गया।

  • प्रोजेक्ट गुटेनबर्ग से उलेसेस डाउनलोड करें :wget http://www.gutenberg.org/files/4300/4300-0.txt
  • इसे 64 बार संक्षिप्त करें: for i in {1..64}; do cat 4300-0.txt >> ulysses64; done
  • 968832 दिखावे के साथ सबसे अक्सर शब्द "" है।

परीक्षण 2: विशेष रूप से उत्पन्न यादृच्छिक पाठ giganovel(लगभग 1 जीबी)।

  • अजगर 3 जनरेटर स्क्रिप्ट यहाँ
  • पाठ में प्राकृतिक भाषाओं के समान दिखने वाले 148391 अलग-अलग शब्द हैं।
  • सबसे अक्सर शब्द: "ई" (11309 दिखावे) और "आईहिट" (11290 उपस्थिति)।

सामान्य परीक्षण: मनमाने ढंग से बड़े अंतराल के साथ बड़े शब्द।

संदर्भ कार्यान्वयन

में देखने के बाद Rosetta कोड इस समस्या के लिए और एहसास है कि कई कार्यान्वयन अविश्वसनीय रूप से धीमी गति से कर रहे हैं (खोल स्क्रिप्ट की तुलना में धीमी!), मैं कुछ अच्छा कार्यान्वयन का परीक्षण यहां । नीचे ulysses64समय जटिलता के साथ प्रदर्शन के लिए नीचे दिया गया है :

                                     ulysses64      Time complexity
C++ (prefix trie + heap)             4.145          O((N + k) log k)
Python (Counter)                     10.547         O(N + k log Q)
AWK + sort                           20.606         O(N + Q log Q)
McIlroy (tr + sort + uniq)           43.554         O(N log N)

क्या आप उसे हरा सकते हैं?

परिक्षण

मानक यूनिक्स के साथ 2017 13 "मैकबुक प्रो का उपयोग करके प्रदर्शन का मूल्यांकन किया जाएगा time कमांड (" उपयोगकर्ता "समय) के साथ किया जाएगा। यदि संभव हो तो, कृपया आधुनिक संकलक का उपयोग करें (जैसे, नवीनतम हास्केल संस्करण का उपयोग करें, न कि विरासत एक)।

अब तक की रैंकिंग

संदर्भ कार्यक्रमों सहित समय:

                                              k=10                  k=100K
                                     ulysses64      giganovel      giganovel
C++ (trie) by ShreevatsaR            0.671          4.227          4.276
C (trie + bins) by Moogie            0.704          9.568          9.459
C (trie + list) by Moogie            0.767          6.051          82.306
C++ (hash trie) by ShreevatsaR       0.788          5.283          5.390
C (trie + sorted list) by Moogie     0.804          7.076          x
Rust (trie) by Anders Kaseorg        0.842          6.932          7.503
J by miles                           1.273          22.365         22.637
C# (trie) by recursive               3.722          25.378         24.771
C++ (trie + heap)                    4.145          42.631         72.138
APL (Dyalog Unicode) by Adám         7.680          x              x
Python (dict) by movatica            9.387          99.118         100.859
Python (Counter)                     10.547         102.822        103.930
Ruby (tally) by daniero              15.139         171.095        171.551
AWK + sort                           20.606         213.366        222.782
McIlroy (tr + sort + uniq)           43.554         715.602        750.420

संचयी रैंकिंग * (%, सर्वोत्तम संभव स्कोर - 300):

#     Program                         Score  Generality
 1  C++ (trie) by ShreevatsaR           300     Yes
 2  C++ (hash trie) by ShreevatsaR      368      x
 3  Rust (trie) by Anders Kaseorg       465     Yes
 4  C (trie + bins) by Moogie           552      x
 5  J by miles                         1248     Yes
 6  C# (trie) by recursive             1734      x
 7  C (trie + list) by Moogie          2182      x
 8  C++ (trie + heap)                  3313      x
 9  Python (dict) by movatica          6103     Yes
10  Python (Counter)                   6435     Yes
11  Ruby (tally) by daniero           10316     Yes
12  AWK + sort                        13329     Yes
13  McIlroy (tr + sort + uniq)        40970     Yes

* तीन परीक्षणों में से प्रत्येक में सर्वश्रेष्ठ कार्यक्रमों के सापेक्ष समय प्रदर्शन का योग।

अब तक का सर्वश्रेष्ठ कार्यक्रम: यहां (दूसरा समाधान)


Ulysses पर स्कोर बस समय है? यह निहित है, लेकिन यह स्पष्ट रूप से नहीं कहा गया है
पोस्ट रॉक गार्फ हंटर

@ SriotchilismO'Zaic, अभी के लिए, हाँ। लेकिन आपको पहले परीक्षण मामले पर भरोसा नहीं करना चाहिए क्योंकि बड़े परीक्षण मामलों का पालन हो सकता है। ulysses64 के दोहराए जाने का स्पष्ट नुकसान है: फ़ाइल के 1/64 के बाद कोई नया शब्द नहीं दिखाई देता है। तो, यह बहुत अच्छा परीक्षण मामला नहीं है, लेकिन इसे वितरित करना (या पुन: पेश करना) आसान है।
एंड्री मकुखा

3
मेरा मतलब था कि छिपे हुए परीक्षण के मामले जो आप पहले की बात कर रहे थे। यदि आप अब हैश पोस्ट करते हैं जब आप वास्तविक ग्रंथों को प्रकट करते हैं तो हम यह सुनिश्चित कर सकते हैं कि यह उत्तरों के लिए उचित है और आप किंग-मेकिंग नहीं हैं। हालांकि मुझे लगता है कि यूलिसिस के लिए हैश कुछ उपयोगी है।
पोस्ट रॉक गार्फ हंटर

1
@tsh यह मेरी समझ है: उदाहरण के लिए दो शब्द होंगे e और g
Moogie

1
@AndriyMakukha आह, धन्यवाद वे सिर्फ कीड़े थे; मैंने उन्हें ठीक किया।
एंडर्स कासोर्ग

जवाबों:


5

[सी]

मेरे 2.8 Ghz Xeon W3530 पर टेस्ट 1 के लिए 1.6 सेकंड से कम में चलता है । Windows 7 पर MinGW.org GCC-6.3.0-1 का उपयोग कर निर्मित:

यह इनपुट के रूप में दो तर्क लेता है (पाठ फ़ाइल का पथ और सूची के लिए सबसे अधिक बार शब्दों की संख्या के लिए)

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

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

इसके अलावा, मैंने इसे एक कार्य कंप्यूटर से सबमिट किया है और टेस्ट 2 टेक्स्ट डाउनलोड करने में सक्षम नहीं है। यह संशोधन के बिना इस टेस्ट 2 के साथ काम करना चाहिए, हालांकि MAX_LETTER_INSTANCES मूल्य को बढ़ाना पड़ सकता है।

// comment out TIMING if using external program timing mechanism
#define TIMING 1

// may need to increase if the source text has many unique words
#define MAX_LETTER_INSTANCES 1000000

// increase this if needing to output more top frequent words
#define MAX_TOP_FREQUENT_WORDS 1000

#define false 0
#define true 1
#define null 0

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef TIMING
#include <sys/time.h>
#endif

struct Letter
{
    char mostFrequentWord;
    struct Letter* parent;
    char asciiCode;
    unsigned int count;
    struct Letter* nextLetters[26];
};
typedef struct Letter Letter;

int main(int argc, char *argv[]) 
{
#ifdef TIMING
    struct timeval tv1, tv2;
    gettimeofday(&tv1, null);
#endif

    int k;
    if (argc !=3 || (k = atoi(argv[2])) <= 0 || k> MAX_TOP_FREQUENT_WORDS)
    {
        printf("Usage:\n");
        printf("      WordCount <input file path> <number of most frequent words to find>\n");
        printf("NOTE: upto %d most frequent words can be requested\n\n",MAX_TOP_FREQUENT_WORDS);
        return -1;
    }

    long  file_size;
    long dataLength;
    char* data;

    // read in file contents
    FILE *fptr;
    size_t read_s = 0;  
    fptr = fopen(argv[1], "rb");
    fseek(fptr, 0L, SEEK_END);
    dataLength = ftell(fptr);
    rewind(fptr);
    data = (char*)malloc((dataLength));
    read_s = fread(data, 1, dataLength, fptr);
    if (fptr) fclose(fptr);

    unsigned int chr;
    unsigned int i;

    // working memory of letters
    Letter* letters = (Letter*) malloc(sizeof(Letter) * MAX_LETTER_INSTANCES);
    memset(&letters[0], 0, sizeof( Letter) * MAX_LETTER_INSTANCES);

    // the index of the next unused letter
    unsigned int letterMasterIndex=0;

    // pesudo letter representing the starting point of any word
    Letter* root = &letters[letterMasterIndex++];

    // the current letter in the word being processed
    Letter* currentLetter = root;
    root->mostFrequentWord = false;
    root->count = 0;

    // the next letter to be processed
    Letter* nextLetter = null;

    // store of the top most frequent words
    Letter* topWords[MAX_TOP_FREQUENT_WORDS];

    // initialise the top most frequent words
    for (i = 0; i<k; i++)
    {
        topWords[i]=root;
    }

    unsigned int lowestWordCount = 0;
    unsigned int lowestWordIndex = 0;
    unsigned int highestWordCount = 0;
    unsigned int highestWordIndex = 0;

    // main loop
    for (int j=0;j<dataLength;j++)
    {
        chr = data[j]|0x20; // convert to lower case

        // is a letter?
        if (chr > 96 && chr < 123)
        {
            chr-=97; // translate to be zero indexed
            nextLetter = currentLetter->nextLetters[chr];

            // this is a new letter at this word length, intialise the new letter
            if (nextLetter == null)
            {
                nextLetter = &letters[letterMasterIndex++];
                nextLetter->parent = currentLetter;
                nextLetter->asciiCode = chr;
                currentLetter->nextLetters[chr] = nextLetter;
            }

            currentLetter = nextLetter;
        }
        // not a letter so this means the current letter is the last letter of a word (if any letters)
        else if (currentLetter!=root)
        {

            // increment the count of the full word that this letter represents
            ++currentLetter->count;

            // ignore this word if already identified as a most frequent word
            if (!currentLetter->mostFrequentWord)
            {
                // update the list of most frequent words
                // by replacing the most infrequent top word if this word is more frequent
                if (currentLetter->count> lowestWordCount)
                {
                    currentLetter->mostFrequentWord = true;
                    topWords[lowestWordIndex]->mostFrequentWord = false;
                    topWords[lowestWordIndex] = currentLetter;
                    lowestWordCount = currentLetter->count;

                    // update the index and count of the next most infrequent top word
                    for (i=0;i<k; i++)
                    {
                        // if the topword  is root then it can immediately be replaced by this current word, otherwise test
                        // whether the top word is less than the lowest word count
                        if (topWords[i]==root || topWords[i]->count<lowestWordCount)
                        {
                            lowestWordCount = topWords[i]->count;
                            lowestWordIndex = i;
                        }
                    }
                }
            }

            // reset the letter path representing the word
            currentLetter = root;
        }
    }

    // print out the top frequent words and counts
    char string[256];
    char tmp[256];

    while (k > 0 )
    {
        highestWordCount = 0;
        string[0]=0;
        tmp[0]=0;

        // find next most frequent word
        for (i=0;i<k; i++)
        {
            if (topWords[i]->count>highestWordCount)
            {
                highestWordCount = topWords[i]->count;
                highestWordIndex = i;
            }
        }

        Letter* letter = topWords[highestWordIndex];

        // swap the end top word with the found word and decrement the number of top words
        topWords[highestWordIndex] = topWords[--k];

        if (highestWordCount > 0)
        {
            // construct string of letters to form the word
            while (letter != root)
            {
                memmove(&tmp[1],&string[0],255);
                tmp[0]=letter->asciiCode+97;
                memmove(&string[0],&tmp[0],255);
                letter=letter->parent;
            }

            printf("%u %s\n",highestWordCount,string);
        }
    }

    free( data );
    free( letters );

#ifdef TIMING   
    gettimeofday(&tv2, null);
    printf("\nTime Taken: %f seconds\n", (double) (tv2.tv_usec - tv1.tv_usec)/1000000 + (double) (tv2.tv_sec - tv1.tv_sec));
#endif
    return 0;
}

टेस्ट 1 के लिए, और शीर्ष 10 लगातार शब्दों के लिए और समय सक्षम होने के साथ इसे प्रिंट करना चाहिए:

 968832 the
 528960 of
 466432 and
 421184 a
 322624 to
 320512 in
 270528 he
 213120 his
 191808 i
 182144 s

 Time Taken: 1.549155 seconds

प्रभावशाली! माना जाता है कि सूची का उपयोग सबसे खराब स्थिति में इसे O (Nk) बनाता है, इसलिए यह k = 100,000 के साथ गिगावेल के लिए संदर्भ C ++ प्रोग्राम की तुलना में धीमा चलता है। लेकिन k << N के लिए यह एक स्पष्ट विजेता है।
एंड्री मकुखा

1
@AndriyMakukha धन्यवाद! मुझे थोड़ा आश्चर्य हुआ कि इस तरह के एक सरल कार्यान्वयन से बड़ी तेजी मिली। मैं सूची को क्रमबद्ध करके k के बड़े मूल्यों के लिए इसे बेहतर बना सकता था। (सॉर्टिंग बहुत महंगी नहीं होनी चाहिए क्योंकि सूची क्रम धीरे-धीरे बदल रहा होगा) लेकिन इससे जटिलता बढ़ जाती है, और संभवतः k के कम मूल्यों के लिए गति प्रभावित होगी। प्रयोग करना होगा
Moogie

हाँ, मैं भी हैरान था। ऐसा इसलिए हो सकता है क्योंकि संदर्भ प्रोग्राम फ़ंक्शन के बहुत सारे कॉल का उपयोग करता है और कंपाइलर इसे ठीक से ऑप्टिमाइज़ करने में विफल रहता है।
एंड्री मकुखा

एक और प्रदर्शन लाभ संभवतः lettersसरणी के शब्दार्थ आवंटन से आता है , जबकि संदर्भ कार्यान्वयन गतिशील रूप से ट्री नोड आवंटित करता है।
एंड्री मकुखा

mmapआईएनजी होना चाहिए तेजी से (~ मेरी linux लैपटॉप पर 5%): #include<sys/mman.h>, <sys/stat.h>, <fcntl.h>, फ़ाइल के साथ पढ़ने की जगह int d=open(argv[1],0);struct stat s;fstat(d,&s);dataLength=s.st_size;data=mmap(0,dataLength,1,1,d,0);और टिप्पणी बाहरfree(data);
NGN

4

जंग

मेरे कंप्यूटर पर, यह Moogie के C "उपसर्ग वृक्ष + डिब्बे" C समाधान की तुलना में 42% तेजी (10.64 s बनाम 18.24 s) के बारे में गीगाबेल 100000 चलाता है। इसके अलावा इसकी कोई पूर्वनिर्धारित सीमा नहीं है (C समाधान के विपरीत जो शब्द की लंबाई, अद्वितीय शब्द, दोहराए गए शब्द आदि पर पूर्वनिर्धारित सीमाएं हैं)।

src/main.rs

use memmap::MmapOptions;
use pdqselect::select_by_key;
use std::cmp::Reverse;
use std::default::Default;
use std::env::args;
use std::fs::File;
use std::io::{self, Write};
use typed_arena::Arena;

#[derive(Default)]
struct Trie<'a> {
    nodes: [Option<&'a mut Trie<'a>>; 26],
    count: u64,
}

fn main() -> io::Result<()> {
    // Parse arguments
    let mut args = args();
    args.next().unwrap();
    let filename = args.next().unwrap();
    let size = args.next().unwrap().parse().unwrap();

    // Open input
    let file = File::open(filename)?;
    let mmap = unsafe { MmapOptions::new().map(&file)? };

    // Build trie
    let arena = Arena::new();
    let mut num_words = 0;
    let mut root = Trie::default();
    {
        let mut node = &mut root;
        for byte in &mmap[..] {
            let letter = (byte | 32).wrapping_sub(b'a');
            if let Some(child) = node.nodes.get_mut(letter as usize) {
                node = child.get_or_insert_with(|| {
                    num_words += 1;
                    arena.alloc(Default::default())
                });
            } else {
                node.count += 1;
                node = &mut root;
            }
        }
        node.count += 1;
    }

    // Extract all counts
    let mut index = 0;
    let mut counts = Vec::with_capacity(num_words);
    let mut stack = vec![root.nodes.iter()];
    'a: while let Some(frame) = stack.last_mut() {
        while let Some(child) = frame.next() {
            if let Some(child) = child {
                if child.count != 0 {
                    counts.push((child.count, index));
                    index += 1;
                }
                stack.push(child.nodes.iter());
                continue 'a;
            }
        }
        stack.pop();
    }

    // Find frequent counts
    select_by_key(&mut counts, size, |&(count, _)| Reverse(count));
    // Or, in nightly Rust:
    //counts.partition_at_index_by_key(size, |&(count, _)| Reverse(count));

    // Extract frequent words
    let size = size.min(counts.len());
    counts[0..size].sort_by_key(|&(_, index)| index);
    let mut out = Vec::with_capacity(size);
    let mut it = counts[0..size].iter();
    if let Some(mut next) = it.next() {
        index = 0;
        stack.push(root.nodes.iter());
        let mut word = vec![b'a' - 1];
        'b: while let Some(frame) = stack.last_mut() {
            while let Some(child) = frame.next() {
                *word.last_mut().unwrap() += 1;
                if let Some(child) = child {
                    if child.count != 0 {
                        if index == next.1 {
                            out.push((word.to_vec(), next.0));
                            if let Some(next1) = it.next() {
                                next = next1;
                            } else {
                                break 'b;
                            }
                        }
                        index += 1;
                    }
                    stack.push(child.nodes.iter());
                    word.push(b'a' - 1);
                    continue 'b;
                }
            }
            stack.pop();
            word.pop();
        }
    }
    out.sort_by_key(|&(_, count)| Reverse(count));

    // Print results
    let stdout = io::stdout();
    let mut stdout = io::BufWriter::new(stdout.lock());
    for (word, count) in out {
        stdout.write_all(&word)?;
        writeln!(stdout, " {}", count)?;
    }

    Ok(())
}

Cargo.toml

[package]
name = "frequent"
version = "0.1.0"
authors = ["Anders Kaseorg <andersk@mit.edu>"]
edition = "2018"

[dependencies]
memmap = "0.7.0"
typed-arena = "1.4.1"
pdqselect = "0.1.0"

[profile.release]
lto = true
opt-level = 3

प्रयोग

cargo build --release
time target/release/frequent ulysses64 10

1
उत्तम! तीनों सेटिंग्स में बहुत अच्छा प्रदर्शन। मैं अभी हाल ही में कैरोल निकोल्स द्वारा रस्ट के बारे में एक हालिया बातचीत देखने के बीच में था :) कुछ हद तक असामान्य वाक्यविन्यास, लेकिन मैं भाषा सीखने के लिए उत्साहित हूं: सी-सी ++ सिस्टम भाषाओं में से एकमात्र ऐसी भाषा लगती है जो ऐसा नहीं करती है डेवलपर के जीवन को बहुत आसान बनाते हुए अधिक प्रदर्शन का त्याग करें।
एंड्री मकुखा

जल्दी से! मैं प्रभावित हु! मुझे आश्चर्य है कि अगर सी (पेड़ + बिन) के लिए बेहतर संकलक विकल्प एक समान परिणाम देगा?
मोगी

@ नमोगी मैं पहले से ही आपका परीक्षण कर रहा था -O3, और -Ofastएक औसत दर्जे का अंतर नहीं करता है।
एंडर्स कासोर्ग

@ उमोजी, मैं आपके कोड की तरह संकलन कर रहा था gcc -O3 -march=native -mtune=native program.c
एंड्री मकुखा

@ अद्रिय मख्खा आह। इससे आपको मेरे परिणाम बनाम परिणामों के बीच गति में बड़े अंतर की व्याख्या होगी: आप पहले से ही अनुकूलन झंडे लगा रहे थे। मुझे नहीं लगता कि कई बड़े कोड अनुकूलन बाकी हैं। मैं दूसरों के द्वारा सुझाए गए नक्शे का उपयोग करके परीक्षण नहीं कर सकता, क्योंकि मिंगव की मृत्यु एक कार्यान्वयन नहीं है ... और केवल 5% की वृद्धि देगा। मुझे लगता है कि मुझे एंडर्स की जबरदस्त एंट्री करनी पड़ेगी। बहुत बढ़िया!
मोगी

3

एपीएल (डायलॉग यूनिकोड)

विंडोज 2.6 के लिए 64-बिट Dyalog APL 17.0 का उपयोग करके मेरे 2.6 Ghz i7-4720HQ पर 8 सेकंड से कम समय में निम्नलिखित चलता है:

⎕{m[⍺↑⍒⊢/m←{(⊂⎕UCS⊃⍺),≢⍵}⌸(⊢⊆⍨96∘<∧<∘123)83⎕DR 819⌶80 ¯1⎕MAP⍵;]}⍞

यह पहले फ़ाइल नाम के लिए संकेत देता है, फिर k के लिए। ध्यान दें कि रनिंग टाइम (लगभग 1 सेकंड) का एक महत्वपूर्ण हिस्सा सिर्फ फाइल को पढ़ रहा है।

इसे करने के लिए, आपको निम्नलिखित को अपने dyalogनिष्पादन योग्य (दस सबसे लगातार शब्दों के लिए) में पाइप करने में सक्षम होना चाहिए :

⎕{m[⍺↑⍒⊢/m←{(⊂⎕UCS⊃⍺),≢⍵}⌸(⊢⊆⍨96∘<∧<∘123)83⎕DR 819⌶80 ¯1⎕MAP⍵;]}⍞
/tmp/ulysses64
10
⎕OFF

इसे प्रिंट करना चाहिए:

 the  968832
 of   528960
 and  466432
 a    421184
 to   322624
 in   320512
 he   270528
 his  213120
 i    191808
 s    182144

बहुत अच्छा! यह अजगर को मारता है। इसके बाद सबसे अच्छा काम किया export MAXWS=4096M। मुझे लगता है, यह हैश तालिकाओं का उपयोग करता है? क्योंकि कार्यक्षेत्र का आकार 2 जीबी तक कम करने से यह पूरे 2 सेकंड तक धीमा हो जाता है।
एंड्री मकुखा

@AndriyMakukha हाँ, इस के अनुसार एक हैश तालिका का उपयोग करता है , और मुझे पूरा यकीन है कि आंतरिक रूप से भी करता है।
Adám

यह ओ (एन लॉग एन) क्यों है? पाइथन की तरह लग रहा है (सभी अद्वितीय शब्दों के ढेर को पुनर्स्थापित करने का समय) या AWK (केवल अद्वितीय शब्दों को छाँटकर) मेरे लिए समाधान। जब तक आप सभी शब्दों को क्रमबद्ध नहीं करते, जैसे कि McIlroy की शेल स्क्रिप्ट में, यह O (N log N) नहीं होना चाहिए।
एंड्री मकुखा

@AndriyMakukha यह ग्रेड सब मायने रखता है। यहाँ हमारे प्रदर्शन के आदमी ने मुझे लिखा है: समय जटिलता हे (एन लॉग एन) है, जब तक कि आप हैश तालिकाओं के बारे में कुछ सैद्धांतिक रूप से संदिग्ध चीजों पर विश्वास नहीं करते हैं, उस स्थिति में यह ओ (एन) है।
Adám

ठीक है, जब मैं 8, 16 और 32 यूलिसिस के खिलाफ आपका कोड चलाता हूं, तो यह बिल्कुल रैखिक रूप से धीमा हो जाता है। हो सकता है कि आपके प्रदर्शन पुरुष को हैश टेबल्स टाइम जटिलता पर अपने विचारों पर पुनर्विचार करने की आवश्यकता हो :) इसके अलावा, यह कोड बड़े परीक्षण के मामले में काम नहीं करता है। यह रिटर्न करता है WS FULL, भले ही मैंने कार्य स्थान को 6 जीबी तक बढ़ा दिया हो।
एंड्री मकुखा

2

[सी] उपसर्ग ट्री + डिब्बे

नोट: संकलक का उपयोग कार्यक्रम निष्पादन की गति पर एक महत्वपूर्ण प्रभाव पड़ता है! मैंने gcc (MinGW.org GCC-8.2.0-3) 8.2.0 का उपयोग किया है। -OIFT स्विचका उपयोग करते समय , प्रोग्राम सामान्य रूप से संकलित प्रोग्राम की तुलना में लगभग 50% तेज चलता है।

एल्गोरिथम जटिलता

जब से मुझे पता चला है कि मैं जो बिन छँटाई कर रहा हूँ, वह कबूतर के प्रकार का एक रूप है, जिसका अर्थ है कि मैं इस समाधान के बिग ओ जटिलता को निष्क्रिय कर सकता हूँ।

मैं इसकी गणना करता हूं:

Worst Time complexity: O(1 + N + k)
Worst Space complexity: O(26*M + N + n) = O(M + N + n)

Where N is the number of words of the data
and M is the number of letters of the data
and n is the range of pigeon holes
and k is the desired number of sorted words to return
and N<=M

ट्री कंस्ट्रक्शन की जटिलता ट्री ट्रैवर्सल के बराबर होती है, इसलिए किसी भी स्तर पर सही नोड को पार करने के लिए O (1) है (क्योंकि प्रत्येक अक्षर को सीधे नोड पर मैप किया जाता है और हम हमेशा प्रत्येक अक्षर के लिए ट्री के एक स्तर को ही पार करते हैं)

कबूतर होल छँटाई O (N + n) है जहाँ n प्रमुख मानों की श्रेणी है, हालाँकि इस समस्या के लिए, हमें सभी मानों को क्रमबद्ध करने की आवश्यकता नहीं है, केवल k संख्या इसलिए सबसे खराब स्थिति O (N + k) होगी।

एक साथ मिलाने से O (1 + N + k) की पैदावार होती है।

पेड़ निर्माण के लिए अंतरिक्ष जटिलता इस तथ्य के कारण है कि सबसे खराब स्थिति 26 * एम नोड है यदि डेटा में एम शब्द के साथ एक शब्द है जिसमें प्रत्येक संख्या में 26 नोड हैं (वर्णमाला के अक्षरों के लिए)। इस प्रकार O (26 * M) = O (M)

कबूतर छेद के लिए छांटना में O (N + n) की जगह की जटिलता है

एक साथ पैदावार O (26 * M + N + n) = O (M + N + n)

कलन विधि

यह इनपुट के रूप में दो तर्क लेता है (पाठ फ़ाइल का पथ और सूची के लिए सबसे अधिक बार शब्दों की संख्या के लिए)

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

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

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

// comment out TIMING if using external program timing mechanism
#define TIMING 1

// may need to increase if the source text has many unique words
#define MAX_LETTER_INSTANCES 1000000

// may need to increase if the source text has many repeated words
#define MAX_BINS 1000000

// assume maximum of 20 letters in a word... adjust accordingly
#define MAX_LETTERS_IN_A_WORD 20

// assume maximum of 10 letters for the string representation of the bin number... adjust accordingly
#define MAX_LETTERS_FOR_BIN_NAME 10

// maximum number of bytes of the output results
#define MAX_OUTPUT_SIZE 10000000

#define false 0
#define true 1
#define null 0
#define SPACE_ASCII_CODE 32

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef TIMING
#include <sys/time.h>
#endif

struct Letter
{
    //char isAWord;
    struct Letter* parent;
    struct Letter* binElementNext;
    char asciiCode;
    unsigned int count;
    struct Letter* nextLetters[26];
};
typedef struct Letter Letter;

struct Bin
{
  struct Letter* word;
};
typedef struct Bin Bin;


int main(int argc, char *argv[]) 
{
#ifdef TIMING
    struct timeval tv1, tv2;
    gettimeofday(&tv1, null);
#endif

    int k;
    if (argc !=3 || (k = atoi(argv[2])) <= 0)
    {
        printf("Usage:\n");
        printf("      WordCount <input file path> <number of most frequent words to find>\n\n");
        return -1;
    }

    long  file_size;
    long dataLength;
    char* data;

    // read in file contents
    FILE *fptr;
    size_t read_s = 0;  
    fptr = fopen(argv[1], "rb");
    fseek(fptr, 0L, SEEK_END);
    dataLength = ftell(fptr);
    rewind(fptr);
    data = (char*)malloc((dataLength));
    read_s = fread(data, 1, dataLength, fptr);
    if (fptr) fclose(fptr);

    unsigned int chr;
    unsigned int i, j;

    // working memory of letters
    Letter* letters = (Letter*) malloc(sizeof(Letter) * MAX_LETTER_INSTANCES);
    memset(&letters[0], null, sizeof( Letter) * MAX_LETTER_INSTANCES);

    // the memory for bins
    Bin* bins = (Bin*) malloc(sizeof(Bin) * MAX_BINS);
    memset(&bins[0], null, sizeof( Bin) * MAX_BINS);

    // the index of the next unused letter
    unsigned int letterMasterIndex=0;
    Letter *nextFreeLetter = &letters[0];

    // pesudo letter representing the starting point of any word
    Letter* root = &letters[letterMasterIndex++];

    // the current letter in the word being processed
    Letter* currentLetter = root;

    // the next letter to be processed
    Letter* nextLetter = null;

    unsigned int sortedListSize = 0;

    // the count of the most frequent word
    unsigned int maxCount = 0;

    // the count of the current word
    unsigned int wordCount = 0;

////////////////////////////////////////////////////////////////////////////////////////////
// CREATING PREFIX TREE
    j=dataLength;
    while (--j>0)
    {
        chr = data[j]|0x20; // convert to lower case

        // is a letter?
        if (chr > 96 && chr < 123)
        {
            chr-=97; // translate to be zero indexed
            nextLetter = currentLetter->nextLetters[chr];

            // this is a new letter at this word length, intialise the new letter
            if (nextLetter == null)
            {
                ++letterMasterIndex;
                nextLetter = ++nextFreeLetter;
                nextLetter->parent = currentLetter;
                nextLetter->asciiCode = chr;
                currentLetter->nextLetters[chr] = nextLetter;
            }

            currentLetter = nextLetter;
        }
        else
        {
            //currentLetter->isAWord = true;

            // increment the count of the full word that this letter represents
            ++currentLetter->count;

            // reset the letter path representing the word
            currentLetter = root;
        }
    }

////////////////////////////////////////////////////////////////////////////////////////////
// ADDING TO BINS

    j = letterMasterIndex;
    currentLetter=&letters[j-1];
    while (--j>0)
    {

      // is the letter the leaf letter of word?
      if (currentLetter->count>0)
      {
        i = currentLetter->count;
        if (maxCount < i) maxCount = i;

        // add to bin
        currentLetter->binElementNext = bins[i].word;
        bins[i].word = currentLetter;
      }
      --currentLetter;
    }

////////////////////////////////////////////////////////////////////////////////////////////
// PRINTING OUTPUT

    // the memory for output
    char* output = (char*) malloc(sizeof(char) * MAX_OUTPUT_SIZE);
    memset(&output[0], SPACE_ASCII_CODE, sizeof( char) * MAX_OUTPUT_SIZE);
    unsigned int outputIndex = 0;

    // string representation of the current bin number
    char binName[MAX_LETTERS_FOR_BIN_NAME];
    memset(&binName[0], SPACE_ASCII_CODE, MAX_LETTERS_FOR_BIN_NAME);


    Letter* letter;
    Letter* binElement;

    // starting at the bin representing the most frequent word(s) and then iterating backwards...
    for ( i=maxCount;i>0 && k>0;i--)
    {
      // check to ensure that the bin has at least one word
      if ((binElement = bins[i].word) != null)
      {
        // update the bin name
        sprintf(binName,"%u",i);

        // iterate of the words in the bin
        while (binElement !=null && k>0)
        {
          // stop if we have reached the desired number of outputed words
          if (k-- > 0)
          {
              letter = binElement;

              // add the bin name to the output
              memcpy(&output[outputIndex],&binName[0],MAX_LETTERS_FOR_BIN_NAME);
              outputIndex+=MAX_LETTERS_FOR_BIN_NAME;

              // construct string of letters to form the word
               while (letter != root)
              {
                // output the letter to the output
                output[outputIndex++] = letter->asciiCode+97;
                letter=letter->parent;
              }

              output[outputIndex++] = '\n';

              // go to the next word in the bin
              binElement = binElement->binElementNext;
          }
        }
      }
    }

    // write the output to std out
    fwrite(output, 1, outputIndex, stdout);
   // fflush(stdout);

   // free( data );
   // free( letters );
   // free( bins );
   // free( output );

#ifdef TIMING   
    gettimeofday(&tv2, null);
    printf("\nTime Taken: %f seconds\n", (double) (tv2.tv_usec - tv1.tv_usec)/1000000 + (double) (tv2.tv_sec - tv1.tv_sec));
#endif
    return 0;
}

संपादित करें: अब पेड़ के निर्माण और उत्पादन के अनुकूलन के बाद तक आबादी वाले डिब्बे को हटा रहा है।

EDIT2: अब स्पीड ऑप्टिमाइज़ेशन के लिए एरे एक्सेस के बजाय पॉइंटर अंकगणित का उपयोग करना।


वाह! 11 सेकंड में 1 जीबी फ़ाइल से 100,000 सबसे लगातार शब्द ... यह किसी तरह की जादू की चाल की तरह दिखता है।
एंड्री मकुखा

कोई ट्रिक नहीं ... कुशल मेमोरी उपयोग के लिए बस सीपीयू समय ट्रेडिंग। मैं आपके परिणाम पर आश्चर्यचकित हूं ... मेरे पुराने पीसी पर 60 सेकंड से अधिक समय लगता है। मैंने देखा है कि II अनावश्यक तुलना कर रहा है और बिनिंग को तब तक स्थगित कर सकता है जब तक फ़ाइल संसाधित नहीं हो जाती। इसे और भी तेज बनाना चाहिए। मैं जल्द ही इसकी कोशिश करूंगा और अपना जवाब अपडेट करूंगा।
मोगी

@AndriyMakukha मैंने अब सभी शब्दों को संसाधित करने और पेड़ का निर्माण होने तक डिब्बे को आबाद करने से हटा दिया है। यह अनावश्यक तुलना और बिन तत्व हेरफेर से बचा जाता है। मैंने यह भी बदल दिया कि जिस तरह से आउटपुट का निर्माण किया जाता है, मुझे लगता है कि मुद्रण समय की एक महत्वपूर्ण राशि ले रहा था!
Moogie

मेरी मशीन पर इस अपडेट से कोई फर्क नहीं पड़ता। हालांकि, इसने ulysses64एक बार बहुत तेजी से प्रदर्शन किया , इसलिए यह अब एक वर्तमान नेता है।
एंड्री मकुखा

मेरे पीसी के साथ एक अनूठा मुद्दा होना चाहिए :) मैंने इस नए आउटपुट एल्गोरिथ्म का उपयोग करते समय 5 सेकंड की गति देखी
Moogie

2

जे

9!:37 ] 0 _ _ _

'input k' =: _2 {. ARGV
k =: ". k

lower =: a. {~ 97 + i. 26
words =: ((lower , ' ') {~ lower i. ]) (32&OR)&.(a.&i.) fread input
words =: ' ' , words
words =: -.&(s: a:) s: words
uniq =: ~. words
res =: (k <. # uniq) {. \:~ (# , {.)/.~ uniq&i. words
echo@(,&": ' ' , [: }.@": {&uniq)/"1 res

exit 0

स्क्रिप्ट के साथ चलाएं jconsole <script> <input> <k>। उदाहरण के लिए, से उत्पादन giganovelके साथ k=100K:

$ time jconsole solve.ijs giganovel 100000 | head 
11309 e
11290 ihit
11285 ah
11260 ist
11255 aa
11202 aiv
11201 al
11188 an
11187 o
11186 ansa

real    0m13.765s
user    0m11.872s
sys     0m1.786s

उपलब्ध सिस्टम मेमोरी की मात्रा को छोड़कर कोई सीमा नहीं है।


छोटे परीक्षण के मामले के लिए बहुत तेज़! अच्छा! हालांकि, मनमाने ढंग से बड़े शब्दों के लिए यह आउटपुट में शब्दों को काट देता है। मुझे यकीन नहीं है कि अगर एक शब्द में वर्णों की संख्या की सीमा है या अगर यह सिर्फ आउटपुट को अधिक संक्षिप्त बनाने के लिए है।
एंड्री मकुखा

@AndriyMakukha हाँ, ...प्रति पंक्ति उत्पादन ट्रंकेशन के कारण होता है। मैंने सभी ट्रंकेशन को अक्षम करने के लिए शुरुआत में एक लाइन जोड़ी। यह बहुत अधिक मेमोरी का उपयोग करने के बाद से गिगोनोवेल पर धीमा हो जाता है क्योंकि इसमें अधिक अद्वितीय शब्द हैं।
मील

महान! अब यह सामान्यता की परीक्षा पास करता है। और यह मेरी मशीन पर धीमा नहीं पड़ा। वास्तव में, एक मामूली गति थी।
एंड्री मकुखा

2

सी ++ (एक ला नूथ)

मैं उत्सुक था कि नुथ का कार्यक्रम कैसा होगा, इसलिए मैंने उसका (मूल रूप से पास्कल) कार्यक्रम C ++ में अनुवादित किया।

भले ही नुथ का प्राथमिक लक्ष्य गति नहीं था, लेकिन साक्षर प्रोग्रामिंग के अपने वेब सिस्टम को स्पष्ट करने के लिए, कार्यक्रम आश्चर्यजनक रूप से प्रतिस्पर्धी है, और अब तक के किसी भी उत्तर की तुलना में तेजी से समाधान की ओर जाता है। यहाँ उनके कार्यक्रम के अनुवाद (WEB कार्यक्रम के संबंधित "खंड" संख्या " {§24}" जैसे टिप्पणियों में उल्लिखित हैं ):

#include <iostream>
#include <cassert>

// Adjust these parameters based on input size.
const int TRIE_SIZE = 800 * 1000; // Size of the hash table used for the trie.
const int ALPHA = 494441;  // An integer that's approximately (0.61803 * TRIE_SIZE), and relatively prime to T = TRIE_SIZE - 52.
const int kTolerance = TRIE_SIZE / 100;  // How many places to try, to find a new place for a "family" (=bunch of children).

typedef int32_t Pointer;  // [0..TRIE_SIZE), an index into the array of Nodes
typedef int8_t Char;  // We only care about 1..26 (plus two values), but there's no "int5_t".
typedef int32_t Count;  // The number of times a word has been encountered.
// These are 4 separate arrays in Knuth's implementation.
struct Node {
  Pointer link;  // From a parent node to its children's "header", or from a header back to parent.
  Pointer sibling;  // Previous sibling, cyclically. (From smallest child to header, and header to largest child.)
  Count count;  // The number of times this word has been encountered.
  Char ch;  // EMPTY, or 1..26, or HEADER. (For nodes with ch=EMPTY, the link/sibling/count fields mean nothing.)
} node[TRIE_SIZE + 1];
// Special values for `ch`: EMPTY (free, can insert child there) and HEADER (start of family).
const Char EMPTY = 0, HEADER = 27;

const Pointer T = TRIE_SIZE - 52;
Pointer x;  // The `n`th time we need a node, we'll start trying at x_n = (alpha * n) mod T. This holds current `x_n`.
// A header can only be in T (=TRIE_SIZE-52) positions namely [27..TRIE_SIZE-26].
// This transforms a "h" from range [0..T) to the above range namely [27..T+27).
Pointer rerange(Pointer n) {
  n = (n % T) + 27;
  // assert(27 <= n && n <= TRIE_SIZE - 26);
  return n;
}

// Convert trie node to string, by walking up the trie.
std::string word_for(Pointer p) {
  std::string word;
  while (p != 0) {
    Char c = node[p].ch;  // assert(1 <= c && c <= 26);
    word = static_cast<char>('a' - 1 + c) + word;
    // assert(node[p - c].ch == HEADER);
    p = (p - c) ? node[p - c].link : 0;
  }
  return word;
}

// Increment `x`, and declare `h` (the first position to try) and `last_h` (the last position to try). {§24}
#define PREPARE_X_H_LAST_H x = (x + ALPHA) % T; Pointer h = rerange(x); Pointer last_h = rerange(x + kTolerance);
// Increment `h`, being careful to account for `last_h` and wraparound. {§25}
#define INCR_H { if (h == last_h) { std::cerr << "Hit tolerance limit unfortunately" << std::endl; exit(1); } h = (h == TRIE_SIZE - 26) ? 27 : h + 1; }

// `p` has no children. Create `p`s family of children, with only child `c`. {§27}
Pointer create_child(Pointer p, int8_t c) {
  // Find `h` such that there's room for both header and child c.
  PREPARE_X_H_LAST_H;
  while (!(node[h].ch == EMPTY and node[h + c].ch == EMPTY)) INCR_H;
  // Now create the family, with header at h and child at h + c.
  node[h]     = {.link = p, .sibling = h + c, .count = 0, .ch = HEADER};
  node[h + c] = {.link = 0, .sibling = h,     .count = 0, .ch = c};
  node[p].link = h;
  return h + c;
}

// Move `p`'s family of children to a place where child `c` will also fit. {§29}
void move_family_for(const Pointer p, Char c) {
  // Part 1: Find such a place: need room for `c` and also all existing children. {§31}
  PREPARE_X_H_LAST_H;
  while (true) {
    INCR_H;
    if (node[h + c].ch != EMPTY) continue;
    Pointer r = node[p].link;
    int delta = h - r;  // We'd like to move each child by `delta`
    while (node[r + delta].ch == EMPTY and node[r].sibling != node[p].link) {
      r = node[r].sibling;
    }
    if (node[r + delta].ch == EMPTY) break;  // There's now space for everyone.
  }

  // Part 2: Now actually move the whole family to start at the new `h`.
  Pointer r = node[p].link;
  int delta = h - r;
  do {
    Pointer sibling = node[r].sibling;
    // Move node from current position (r) to new position (r + delta), and free up old position (r).
    node[r + delta] = {.ch = node[r].ch, .count = node[r].count, .link = node[r].link, .sibling = node[r].sibling + delta};
    if (node[r].link != 0) node[node[r].link].link = r + delta;
    node[r].ch = EMPTY;
    r = sibling;
  } while (node[r].ch != EMPTY);
}

// Advance `p` to its `c`th child. If necessary, add the child, or even move `p`'s family. {§21}
Pointer find_child(Pointer p, Char c) {
  // assert(1 <= c && c <= 26);
  if (p == 0) return c;  // Special case for first char.
  if (node[p].link == 0) return create_child(p, c);  // If `p` currently has *no* children.
  Pointer q = node[p].link + c;
  if (node[q].ch == c) return q;  // Easiest case: `p` already has a `c`th child.
  // Make sure we have room to insert a `c`th child for `p`, by moving its family if necessary.
  if (node[q].ch != EMPTY) {
    move_family_for(p, c);
    q = node[p].link + c;
  }
  // Insert child `c` into `p`'s family of children (at `q`), with correct siblings. {§28}
  Pointer h = node[p].link;
  while (node[h].sibling > q) h = node[h].sibling;
  node[q] = {.ch = c, .count = 0, .link = 0, .sibling = node[h].sibling};
  node[h].sibling = q;
  return q;
}

// Largest descendant. {§18}
Pointer last_suffix(Pointer p) {
  while (node[p].link != 0) p = node[node[p].link].sibling;
  return p;
}

// The largest count beyond which we'll put all words in the same (last) bucket.
// We do an insertion sort (potentially slow) in last bucket, so increase this if the program takes a long time to walk trie.
const int MAX_BUCKET = 10000;
Pointer sorted[MAX_BUCKET + 1];  // The head of each list.

// Records the count `n` of `p`, by inserting `p` in the list that starts at `sorted[n]`.
// Overwrites the value of node[p].sibling (uses the field to mean its successor in the `sorted` list).
void record_count(Pointer p) {
  // assert(node[p].ch != HEADER);
  // assert(node[p].ch != EMPTY);
  Count f = node[p].count;
  if (f == 0) return;
  if (f < MAX_BUCKET) {
    // Insert at head of list.
    node[p].sibling = sorted[f];
    sorted[f] = p;
  } else {
    Pointer r = sorted[MAX_BUCKET];
    if (node[p].count >= node[r].count) {
      // Insert at head of list
      node[p].sibling = r;
      sorted[MAX_BUCKET] = p;
    } else {
      // Find right place by count. This step can be SLOW if there are too many words with count >= MAX_BUCKET
      while (node[p].count < node[node[r].sibling].count) r = node[r].sibling;
      node[p].sibling = node[r].sibling;
      node[r].sibling = p;
    }
  }
}

// Walk the trie, going over all words in reverse-alphabetical order. {§37}
// Calls "record_count" for each word found.
void walk_trie() {
  // assert(node[0].ch == HEADER);
  Pointer p = node[0].sibling;
  while (p != 0) {
    Pointer q = node[p].sibling;  // Saving this, as `record_count(p)` will overwrite it.
    record_count(p);
    // Move down to last descendant of `q` if any, else up to parent of `q`.
    p = (node[q].ch == HEADER) ? node[q].link : last_suffix(q);
  }
}

int main(int, char** argv) {
  // Program startup
  std::ios::sync_with_stdio(false);

  // Set initial values {§19}
  for (Char i = 1; i <= 26; ++i) node[i] = {.ch = i, .count = 0, .link = 0, .sibling = i - 1};
  node[0] = {.ch = HEADER, .count = 0, .link = 0, .sibling = 26};

  // read in file contents
  FILE *fptr = fopen(argv[1], "rb");
  fseek(fptr, 0L, SEEK_END);
  long dataLength = ftell(fptr);
  rewind(fptr);
  char* data = (char*)malloc(dataLength);
  fread(data, 1, dataLength, fptr);
  if (fptr) fclose(fptr);

  // Loop over file contents: the bulk of the time is spent here.
  Pointer p = 0;
  for (int i = 0; i < dataLength; ++i) {
    Char c = (data[i] | 32) - 'a' + 1;  // 1 to 26, for 'a' to 'z' or 'A' to 'Z'
    if (1 <= c && c <= 26) {
      p = find_child(p, c);
    } else {
      ++node[p].count;
      p = 0;
    }
  }
  node[0].count = 0;

  walk_trie();

  const int max_words_to_print = atoi(argv[2]);
  int num_printed = 0;
  for (Count f = MAX_BUCKET; f >= 0 && num_printed <= max_words_to_print; --f) {
    for (Pointer p = sorted[f]; p != 0 && num_printed < max_words_to_print; p = node[p].sibling) {
      std::cout << word_for(p) << " " << node[p].count << std::endl;
      ++num_printed;
    }
  }

  return 0;
}

नुथ के कार्यक्रम से अंतर:

  • मैं नुथ के 4 सरणियों संयुक्त link, sibling, countऔर chएक की एक सरणी मेंstruct Node (यह आसान इस तरह से समझने के लिए लगता है)।
  • मैंने साक्षर-प्रोग्रामिंग (WEB- शैली) को अनुभागों के अधिक पारंपरिक फ़ंक्शन कॉल (और मैक्रोज़ के एक जोड़े) में अनुभागीय परिवर्तन को बदल दिया।
  • हमें मानक पास्कल के अजीब I / O सम्मेलनों / प्रतिबंधों का उपयोग करने की आवश्यकता नहीं है, इसलिए उपयोग करना freadऔरdata[i] | 32 - 'a' अन्य उत्तर यहाँ के रूप में, पास्कल वैकल्पिक हल के बजाय।
  • यदि कार्यक्रम चल रहा है, तो हम सीमा से अधिक (अंतरिक्ष से बाहर), नथ के मूल कार्यक्रम बाद के शब्दों को छोड़ने और अंत में एक संदेश मुद्रित करने के लिए इसे शान से करते हैं। (यह कहना बिलकुल सही नहीं है कि मैक्लीरो ने "नथ के समाधान की आलोचना की, क्योंकि वह बाइबल के पूर्ण पाठ को संसाधित करने में सक्षम नहीं थे"; वह केवल इस ओर इशारा कर रहा था कि कभी-कभी किसी पाठ में बहुत देर हो सकती है, जैसे कि यीशु शब्द "बाइबल में, इसलिए त्रुटि की स्थिति सहज नहीं है।) मैंने नोइज़ियर (और वैसे भी आसान) कार्यक्रम को समाप्त करने के दृष्टिकोण को लिया है।
  • कार्यक्रम स्मृति उपयोग को नियंत्रित करने के लिए एक निरंतर TRIE_SIZE घोषित करता है, जिसे मैंने टक्कर दी थी। (मूल आवश्यकताओं के लिए 32767 का स्थिरांक चुना गया था - "एक उपयोगकर्ता को बीस-पृष्ठ तकनीकी पेपर में 100 सबसे लगातार शब्दों को खोजने में सक्षम होना चाहिए (लगभग 50K बाइट फ़ाइल)" और क्योंकि पास्कल पूर्णांक पूर्णांक के साथ अच्छी तरह से व्यवहार करता है। प्रकार और उन्हें बेहतर तरीके से पैक करते हैं। हमें इसे 25x से 800,000 तक बढ़ाना पड़ा क्योंकि परीक्षण इनपुट अब 20 मिलियन गुना बड़ा है।)
  • स्ट्रिंग्स के अंतिम मुद्रण के लिए, हम बस ट्राइ कर सकते हैं और एक गूंगा (संभवतः द्विघात) स्ट्रिंग एपेंड भी कर सकते हैं।

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

समय

के साथ संकलित किया

clang++ -std=c++17 -O2 ptrie-walktrie.cc 

जब यहां सबसे बड़े टेस्टकेस पर चला जाता है ( giganovel100,000 शब्दों के साथ अनुरोध किया जाता है), और यहां अब तक पोस्ट किए गए सबसे तेज कार्यक्रम की तुलना में, मुझे यह थोड़ा या लगातार तेज लगता है:

target/release/frequent:   4.809 ±   0.263 [ 4.45.. 5.62]        [... 4.63 ...  4.75 ...  4.88...]
ptrie-walktrie:            4.547 ±   0.164 [ 4.35.. 4.99]        [... 4.42 ...   4.5 ...  4.68...]

(शीर्ष पंक्ति एंडर्स कसेर्ग के जंग समाधान है; नीचे का उपरोक्त कार्यक्रम है। ये मीन, अधिकतम, मध्य और चतुर्थक के साथ 100 रन से समयावधि हैं।)

विश्लेषण

यह तेज क्यों है? ऐसा नहीं है कि C ++ रस्ट की तुलना में तेज़ है, या कि नुथ का कार्यक्रम सबसे तेज़ संभव है - वास्तव में, नथुथ का कार्यक्रम आवेषण पर (जैसा कि वह उल्लेख करता है) ट्राइ-पैकिंग (मेमोरी को संरक्षित करने के लिए) के कारण धीमा है। कारण, मुझे संदेह है, कुछ इस से संबंधित है कि नूथ ने 2008 में शिकायत की थी :

64-बिट पॉइंटर्स के बारे में एक ज्वाला

जब मैंने 4 गीगाबाइट से कम रैम का उपयोग करने वाले प्रोग्राम को संकलित किया, तो 64-बिट पॉइंटर्स होना बिल्कुल मुहावरा है। जब इस तरह के पॉइंटर मान किसी संरचना के अंदर दिखाई देते हैं, तो वे न केवल आधी मेमोरी बर्बाद करते हैं, वे प्रभावी रूप से कैश के आधे भाग को फेंक देते हैं।

ऊपर दिए गए कार्यक्रम में 32-बिट ऐरे इंडिकेशंस (64-बिट पॉइंटर्स नहीं) का उपयोग किया गया है, इसलिए "नोड" संरचना कम मेमोरी में रहती है, इसलिए स्टैक पर अधिक नोड्स और कम कैश मिस हैं। (वास्तव में, वहाँ था कुछ काम के रूप में इस पर x32 ABI , लेकिन यह प्रतीत हो रहा है एक अच्छी स्थिति में नहीं है, भले ही विचार स्पष्ट रूप से उपयोगी है, उदाहरण के लिए देखने के हाल ही में घोषणा की वी 8 में सूचक संपीड़न । ओह अच्छा।) तो पर giganovel, यह प्रोग्राम (पैक्ड) ट्राइ के लिए 12.8 एमबी का उपयोग करता है, बनाम रस्ट प्रोग्राम के 32.18 एमबी के लिए इसके ट्राइ (ऑन giganovel) के लिए। हम 1000x ("गिगोनवेल" से "टेरानोवेल" तक) कह सकते हैं और अभी भी 32-बिट सूचकांकों से अधिक नहीं हो सकते हैं, इसलिए यह एक उचित विकल्प लगता है।

तेज़ वैरिएंट

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

#include <iostream>
#include <cassert>
#include <vector>
#include <algorithm>

typedef int32_t Pointer;  // [0..node.size()), an index into the array of Nodes
typedef int32_t Count;
typedef int8_t Char;  // We'll usually just have 1 to 26.
struct Node {
  Pointer link;  // From a parent node to its children's "header", or from a header back to parent.
  Count count;  // The number of times this word has been encountered. Undefined for header nodes.
};
std::vector<Node> node; // Our "arena" for Node allocation.

std::string word_for(Pointer p) {
  std::vector<char> drow;  // The word backwards
  while (p != 0) {
    Char c = p % 27;
    drow.push_back('a' - 1 + c);
    p = (p - c) ? node[p - c].link : 0;
  }
  return std::string(drow.rbegin(), drow.rend());
}

// `p` has no children. Create `p`s family of children, with only child `c`.
Pointer create_child(Pointer p, Char c) {
  Pointer h = node.size();
  node.resize(node.size() + 27);
  node[h] = {.link = p, .count = -1};
  node[p].link = h;
  return h + c;
}

// Advance `p` to its `c`th child. If necessary, add the child.
Pointer find_child(Pointer p, Char c) {
  assert(1 <= c && c <= 26);
  if (p == 0) return c;  // Special case for first char.
  if (node[p].link == 0) return create_child(p, c);  // Case 1: `p` currently has *no* children.
  return node[p].link + c;  // Case 2 (easiest case): Already have the child c.
}

int main(int, char** argv) {
  auto start_c = std::clock();

  // Program startup
  std::ios::sync_with_stdio(false);

  // read in file contents
  FILE *fptr = fopen(argv[1], "rb");
  fseek(fptr, 0, SEEK_END);
  long dataLength = ftell(fptr);
  rewind(fptr);
  char* data = (char*)malloc(dataLength);
  fread(data, 1, dataLength, fptr);
  fclose(fptr);

  node.reserve(dataLength / 600);  // Heuristic based on test data. OK to be wrong.
  node.push_back({0, 0});
  for (Char i = 1; i <= 26; ++i) node.push_back({0, 0});

  // Loop over file contents: the bulk of the time is spent here.
  Pointer p = 0;
  for (long i = 0; i < dataLength; ++i) {
    Char c = (data[i] | 32) - 'a' + 1;  // 1 to 26, for 'a' to 'z' or 'A' to 'Z'
    if (1 <= c && c <= 26) {
      p = find_child(p, c);
    } else {
      ++node[p].count;
      p = 0;
    }
  }
  ++node[p].count;
  node[0].count = 0;

  // Brute-force: Accumulate all words and their counts, then sort by frequency and print.
  std::vector<std::pair<int, std::string>> counts_words;
  for (Pointer i = 1; i < static_cast<Pointer>(node.size()); ++i) {
    int count = node[i].count;
    if (count == 0 || i % 27 == 0) continue;
    counts_words.push_back({count, word_for(i)});
  }
  auto cmp = [](auto x, auto y) {
    if (x.first != y.first) return x.first > y.first;
    return x.second < y.second;
  };
  std::sort(counts_words.begin(), counts_words.end(), cmp);
  const int max_words_to_print = std::min<int>(counts_words.size(), atoi(argv[2]));
  for (int i = 0; i < max_words_to_print; ++i) {
    auto [count, word] = counts_words[i];
    std::cout << word << " " << count << std::endl;
  }

  return 0;
}

यह कार्यक्रम, यहां समाधानों की तुलना में बहुत कुछ करने के लिए giganovelपर्याप्त है, बावजूद इसके ट्राइ के लिए केवल 12.2MB का उपयोग करता है (और ) तेज होने का प्रबंधन करता है। इस कार्यक्रम की समयसीमा (अंतिम पंक्ति), पहले बताए गए समयों की तुलना में:

target/release/frequent:   4.809 ±   0.263 [ 4.45.. 5.62]        [... 4.63 ...  4.75 ...  4.88...]
ptrie-walktrie:            4.547 ±   0.164 [ 4.35.. 4.99]        [... 4.42 ...   4.5 ...  4.68...]
itrie-nolimit:             3.907 ±   0.127 [ 3.69.. 4.23]        [... 3.81 ...   3.9 ...   4.0...]

मैं यह देखने के लिए उत्सुक हूं कि यह (या हैश-ट्राइ प्रोग्राम) क्या होगा अगर रुस्त में अनुवाद किया जाए । :-)

आगे की जानकारी

  1. यहां उपयोग किए गए डेटा संरचना के बारे में: TAOCP के खंड 3 में धारा 6.3 (डिजिटल खोज, अर्थात कोशिश) के एक्सरसाइज 4 में "पैकिंग" की कोशिशों का एक विवरण दिया गया है, और टीएक्स में शिफॉन के छात्र फ्रैंक लिआंग की थीसिस के बारे में भी बताया गया है : कॉम-पुट-एर द्वारा शब्द हाय-फिन-ए-टियोन

  2. बेंटले के कॉलम, नूथ के कार्यक्रम, और मैकलरॉय की समीक्षा (यूनिक्स दर्शन के बारे में केवल एक छोटा सा हिस्सा) का संदर्भ पिछले और बाद के स्तंभों के प्रकाश में स्पष्ट है , और नॉथ का पिछला अनुभव संकलक, टीएआरसीपी और टीएक्स सहित है।

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

मेरे पास ऊपर के बिंदुओं पर विस्तृत एक अधूरा ब्लॉग पोस्ट है; ऐसा होने पर इस उत्तर को संपादित कर सकते हैं। इस बीच, नुत के जन्मदिन के अवसर (10 जनवरी) पर, वैसे भी यहाँ इस उत्तर को पोस्ट करना। :-)


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

मेरे पास केवल दो टिप्पणियां हैं: 1) आपका दूसरा कार्यक्रम वर्तमान में Segmentation fault: 11अनियंत्रित रूप से बड़े शब्दों और अंतराल के साथ परीक्षण मामलों के लिए विफल रहता है; 2) भले ही यह महसूस हो कि मैं McIlroy की "समालोचना" के प्रति सहानुभूति रखता हूं, मैं अच्छी तरह से जानता हूं कि नूथ का इरादा केवल अपनी साक्षर प्रोग्रामिंग तकनीक को दिखाने का था, जबकि McIlroy ने इंजीनियरिंग के नजरिए से इसकी आलोचना की। मैकलीरो ने खुद बाद में स्वीकार किया कि यह करना उचित नहीं था।
एंड्री मकुखा

@AndriyMakukha ओह ओप्स, वह पुनरावर्ती थी word_for; अब इसे ठीक कर दिया। हाँ, McIlroy, यूनिक्स पाइप के आविष्कारक के रूप में, छोटे उपकरणों की रचना के यूनिक्स दर्शन को प्रचारित करने का अवसर लिया । यह एक अच्छा दर्शन है, नूथ की निराशा की तुलना में (यदि आप उनके कार्यक्रमों को पढ़ने की कोशिश कर रहे हैं) अखंड दृष्टिकोण, लेकिन संदर्भ में यह थोड़ा अनुचित था, एक और कारण से भी: आज यूनिक्स रास्ता व्यापक रूप से उपलब्ध है, लेकिन 1986 में सीमित था बेल लैब्स, बर्कले, आदि ("उनकी फर्म व्यवसाय में सबसे अच्छा प्रीफ़ैब बनाती है")
श्रीवत्सआर

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

1

अजगर ३

एक सरल शब्दकोश के साथ यह कार्यान्वयन Counterमेरे सिस्टम पर एक का उपयोग करने वाले की तुलना में थोड़ा तेज है ।

def words_from_file(filename):
    import re

    pattern = re.compile('[a-z]+')

    for line in open(filename):
        yield from pattern.findall(line.lower())


def freq(textfile, k):
    frequencies = {}

    for word in words_from_file(textfile):
        frequencies[word] = frequencies.get(word, 0) + 1

    most_frequent = sorted(frequencies.items(), key=lambda item: item[1], reverse=True)

    for i, (word, frequency) in enumerate(most_frequent):
        if i == k:
            break

        yield word, frequency


from time import time

start = time()
print('\n'.join('{}:\t{}'.format(f, w) for w,f in freq('giganovel', 10)))
end = time()
print(end - start)

1
मैं अपने सिस्टम पर केवल गिगानोवेल के साथ परीक्षण कर सकता था, और इसमें काफी लंबा समय लगता है (~ 90 सेकंड)। जर्मनी में कानूनी कारणों से अवरुद्ध है
गुटेनबर्गप्रोजेक्ट

दिलचस्प। यह या तो विधि में heapqकोई प्रदर्शन नहीं जोड़ता है Counter.most_common, या आंतरिक रूप से enumerate(sorted(...))भी उपयोग करता है heapq
एंड्री मकुखा

मैंने पायथन 2 के साथ परीक्षण किया और प्रदर्शन समान था, इसलिए, मुझे लगता है, छंटाई बस के रूप में तेजी से काम करती है, जैसे Counter.most_common
एंड्री मकुक्खा

हाँ, शायद यह मेरे सिस्टम पर घबराना था ... कम से कम यह धीमा नहीं है :) लेकिन regex की खोज पात्रों पर ध्यान देने की तुलना में बहुत तेज है। यह काफी प्रदर्शनकारी के रूप में लागू किया गया लगता है।
जंगी

1

[सी] उपसर्ग ट्री + क्रमबद्ध लिंक्ड सूची

यह इनपुट के रूप में दो तर्क लेता है (पाठ फ़ाइल का पथ और सूची के लिए सबसे अधिक बार शब्दों की संख्या के लिए)

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

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

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

// comment out TIMING if using external program timing mechanism
#define TIMING 1

// may need to increase if the source text has many unique words
#define MAX_LETTER_INSTANCES 1000000

#define false 0
#define true 1
#define null 0

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef TIMING
#include <sys/time.h>
#endif

struct Letter
{
    char isTopWord;
    struct Letter* parent;
    struct Letter* higher;
    struct Letter* lower;
    char asciiCode;
    unsigned int count;
    struct Letter* nextLetters[26];
};
typedef struct Letter Letter;

int main(int argc, char *argv[]) 
{
#ifdef TIMING
    struct timeval tv1, tv2;
    gettimeofday(&tv1, null);
#endif

    int k;
    if (argc !=3 || (k = atoi(argv[2])) <= 0)
    {
        printf("Usage:\n");
        printf("      WordCount <input file path> <number of most frequent words to find>\n\n");
        return -1;
    }

    long  file_size;
    long dataLength;
    char* data;

    // read in file contents
    FILE *fptr;
    size_t read_s = 0;  
    fptr = fopen(argv[1], "rb");
    fseek(fptr, 0L, SEEK_END);
    dataLength = ftell(fptr);
    rewind(fptr);
    data = (char*)malloc((dataLength));
    read_s = fread(data, 1, dataLength, fptr);
    if (fptr) fclose(fptr);

    unsigned int chr;
    unsigned int i;

    // working memory of letters
    Letter* letters = (Letter*) malloc(sizeof(Letter) * MAX_LETTER_INSTANCES);
    memset(&letters[0], 0, sizeof( Letter) * MAX_LETTER_INSTANCES);

    // the index of the next unused letter
    unsigned int letterMasterIndex=0;

    // pesudo letter representing the starting point of any word
    Letter* root = &letters[letterMasterIndex++];

    // the current letter in the word being processed
    Letter* currentLetter = root;

    // the next letter to be processed
    Letter* nextLetter = null;
    Letter* sortedWordsStart = null;
    Letter* sortedWordsEnd = null;
    Letter* A;
    Letter* B;
    Letter* C;
    Letter* D;

    unsigned int sortedListSize = 0;


    unsigned int lowestWordCount = 0;
    unsigned int lowestWordIndex = 0;
    unsigned int highestWordCount = 0;
    unsigned int highestWordIndex = 0;

    // main loop
    for (int j=0;j<dataLength;j++)
    {
        chr = data[j]|0x20; // convert to lower case

        // is a letter?
        if (chr > 96 && chr < 123)
        {
            chr-=97; // translate to be zero indexed
            nextLetter = currentLetter->nextLetters[chr];

            // this is a new letter at this word length, intialise the new letter
            if (nextLetter == null)
            {
                nextLetter = &letters[letterMasterIndex++];
                nextLetter->parent = currentLetter;
                nextLetter->asciiCode = chr;
                currentLetter->nextLetters[chr] = nextLetter;
            }

            currentLetter = nextLetter;
        }
        // not a letter so this means the current letter is the last letter of a word (if any letters)
        else if (currentLetter!=root)
        {

            // increment the count of the full word that this letter represents
            ++currentLetter->count;

            // is this word not in the top word list?
            if (!currentLetter->isTopWord)
            {
                // first word becomes the sorted list
                if (sortedWordsStart == null)
                {
                  sortedWordsStart = currentLetter;
                  sortedWordsEnd = currentLetter;
                  currentLetter->isTopWord = true;
                  ++sortedListSize;
                }
                // always add words until list is at desired size, or 
                // swap the current word with the end of the sorted word list if current word count is larger
                else if (sortedListSize < k || currentLetter->count> sortedWordsEnd->count)
                {
                    // replace sortedWordsEnd entry with current word
                    if (sortedListSize == k)
                    {
                      currentLetter->higher = sortedWordsEnd->higher;
                      currentLetter->higher->lower = currentLetter;
                      sortedWordsEnd->isTopWord = false;
                    }
                    // add current word to the sorted list as the sortedWordsEnd entry
                    else
                    {
                      ++sortedListSize;
                      sortedWordsEnd->lower = currentLetter;
                      currentLetter->higher = sortedWordsEnd;
                    }

                    currentLetter->lower = null;
                    sortedWordsEnd = currentLetter;
                    currentLetter->isTopWord = true;
                }
            }
            // word is in top list
            else
            {
                // check to see whether the current word count is greater than the supposedly next highest word in the list
                // we ignore the word that is sortedWordsStart (i.e. most frequent)
                while (currentLetter != sortedWordsStart && currentLetter->count> currentLetter->higher->count)
                {
                    B = currentLetter->higher;
                    C = currentLetter;
                    A = B != null ? currentLetter->higher->higher : null;
                    D = currentLetter->lower;

                    if (A !=null) A->lower = C;
                    if (D !=null) D->higher = B;
                    B->higher = C;
                    C->higher = A;
                    B->lower = D;
                    C->lower = B;

                    if (B == sortedWordsStart)
                    {
                      sortedWordsStart = C;
                    }

                    if (C == sortedWordsEnd)
                    {
                      sortedWordsEnd = B;
                    }
                }
            }

            // reset the letter path representing the word
            currentLetter = root;
        }
    }

    // print out the top frequent words and counts
    char string[256];
    char tmp[256];

    Letter* letter;
    while (sortedWordsStart != null )
    {
        letter = sortedWordsStart;
        highestWordCount = letter->count;
        string[0]=0;
        tmp[0]=0;

        if (highestWordCount > 0)
        {
            // construct string of letters to form the word
            while (letter != root)
            {
                memmove(&tmp[1],&string[0],255);
                tmp[0]=letter->asciiCode+97;
                memmove(&string[0],&tmp[0],255);
                letter=letter->parent;
            }

            printf("%u %s\n",highestWordCount,string);
        }
        sortedWordsStart = sortedWordsStart->lower;
    }

    free( data );
    free( letters );

#ifdef TIMING   
    gettimeofday(&tv2, null);
    printf("\nTime Taken: %f seconds\n", (double) (tv2.tv_usec - tv1.tv_usec)/1000000 + (double) (tv2.tv_sec - tv1.tv_sec));
#endif
    return 0;
}

यह k = 100,000 के लिए बहुत क्रमित आउटपुट नहीं देता है 12 eroilk 111 iennoa 10 yttelen 110 engyt:।
एंड्री मकुखा

मुझे लगता है कि मेरे पास इसके कारण के रूप में एक विचार है। मेरा विचार यह है कि मुझे वर्तमान शब्द के अगले उच्चतम शब्द की जाँच करते समय सूची में स्वैप शब्दों को पुनरावृत्त करना होगा। जब मेरे पास समय होगा तो मैं जांच करूंगा
Moogie

हम्म यह अच्छी तरह से लगता है कि अगर काम करता है, तो एक को बदलने का सरल फिक्स, हालांकि यह कश्मीर के बड़े मूल्यों के लिए एल्गोरिथ्म को भी धीमा कर देता है। मुझे अधिक चतुर समाधान के बारे में सोचना पड़ सकता है।
मोगी

1

सी#

यह अंतिम .net SDK के साथ काम करना चाहिए

using System;
using System.IO;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using static System.Console;

class Node {
    public Node Parent;
    public Node[] Nodes;
    public int Index;
    public int Count;

    public static readonly List<Node> AllNodes = new List<Node>();

    public Node(Node parent, int index) {
        this.Parent = parent;
        this.Index = index;
        AllNodes.Add(this);
    }

    public Node Traverse(uint u) {
        int b = (int)u;
        if (this.Nodes is null) {
            this.Nodes = new Node[26];
            return this.Nodes[b] = new Node(this, b);
        }
        if (this.Nodes[b] is null) return this.Nodes[b] = new Node(this, b);
        return this.Nodes[b];
    }

    public string GetWord() => this.Index >= 0 
        ? this.Parent.GetWord() + (char)(this.Index + 97)
        : "";
}

class Freq {
    const int DefaultBufferSize = 0x10000;

    public static void Main(string[] args) {
        var sw = Stopwatch.StartNew();

        if (args.Length < 2) {
            WriteLine("Usage: freq.exe {filename} {k} [{buffersize}]");
            return;
        }

        string file = args[0];
        int k = int.Parse(args[1]);
        int bufferSize = args.Length >= 3 ? int.Parse(args[2]) : DefaultBufferSize;

        Node root = new Node(null, -1) { Nodes = new Node[26] }, current = root;
        int b;
        uint u;

        using (var fr = new FileStream(file, FileMode.Open))
        using (var br = new BufferedStream(fr, bufferSize)) {
            outword:
                b = br.ReadByte() | 32;
                if ((u = (uint)(b - 97)) >= 26) {
                    if (b == -1) goto done; 
                    else goto outword;
                }
                else current = root.Traverse(u);
            inword:
                b = br.ReadByte() | 32;
                if ((u = (uint)(b - 97)) >= 26) {
                    if (b == -1) goto done;
                    ++current.Count;
                    goto outword;
                }
                else {
                    current = current.Traverse(u);
                    goto inword;
                }
            done:;
        }

        WriteLine(string.Join("\n", Node.AllNodes
            .OrderByDescending(count => count.Count)
            .Take(k)
            .Select(node => node.GetWord())));

        WriteLine("Self-measured milliseconds: {0}", sw.ElapsedMilliseconds);
    }
}

यहाँ एक नमूना आउटपुट है।

C:\dev\freq>csc -o -nologo freq-trie.cs && freq-trie.exe giganovel 100000
e
ihit
ah
ist
 [... omitted for sanity ...]
omaah
aanhele
okaistai
akaanio
Self-measured milliseconds: 13619

सबसे पहले, मैंने स्ट्रिंग कुंजियों के साथ एक शब्दकोश का उपयोग करने की कोशिश की, लेकिन यह बहुत धीमा था। मुझे लगता है कि यह इसलिए है क्योंकि .net स्ट्रिंग्स को आंतरिक रूप से 2-बाइट एन्कोडिंग के साथ दर्शाया जाता है, जो इस एप्लिकेशन के लिए बेकार है। तो फिर मैं बस शुद्ध बाइट्स, और एक बदसूरत गोटो शैली राज्य मशीन के लिए बंद कर दिया। केस रूपांतरण एक बिटवाइज़ ऑपरेटर है। चरित्र श्रेणी की जाँच घटाव के बाद एकल तुलना में की जाती है। मैंने किसी भी तरह के खर्च को अंतिम रूप देने का प्रयास नहीं किया क्योंकि मैंने पाया कि यह रनटाइम के 0.1% से कम का उपयोग कर रहा है।

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


1
बहुत प्रभावशाली! मुझे आपकी बिटवाइज़ tolowerऔर सिंगल तुलना ट्रिक्स पसंद हैं। हालाँकि, मुझे समझ में नहीं आता है कि आपका कार्यक्रम अपेक्षा से अधिक विशिष्ट शब्दों की रिपोर्ट क्यों करता है। इसके अलावा, मूल समस्या वर्णन के अनुसार, प्रोग्राम को आवृत्ति के घटते क्रम में सभी k शब्दों को आउटपुट करने की आवश्यकता होती है, इसलिए मैंने आपके प्रोग्राम को अंतिम परीक्षा की ओर नहीं गिना, जिसे 100,000 बार-बार सबसे अधिक आउटपुट देने की आवश्यकता है।
एंड्री मकुखा

@AndriyMakukha: मैं देख सकता हूँ कि मैं भी शब्द उपसर्ग गिन रहा हूँ जो अंतिम गिनती में कभी नहीं हुआ। मैंने सभी आउटपुट लिखने से परहेज किया क्योंकि विंडोज़ में कंसोल आउटपुट बहुत धीमा है। क्या मैं किसी फ़ाइल में आउटपुट लिख सकता हूँ?
पुनरावर्ती

कृपया इसे मानक आउटपुट प्रिंट करें। K = 10 के लिए, यह किसी भी मशीन पर तेज़ होना चाहिए। आप कमांड लाइन से फाइल में आउटपुट को रीडायरेक्ट भी कर सकते हैं। इस तरह
एंड्री मकुखा

@AndriyMakukha: मेरा मानना ​​है कि मैंने सभी समस्याओं का समाधान किया है। मुझे बहुत रनटाइम लागत के बिना सभी आवश्यक आउटपुट का एक तरीका मिला।
पुनरावर्ती

यह उत्पादन तेजी से धधक रहा है! बहुत अच्छा। मैंने आपके प्रोग्राम को फ़्रीक्वेंसी काउंट को भी प्रिंट करने के लिए संशोधित किया, जैसा कि अन्य समाधान करते हैं।
एंड्री मकुखा

1

रूबी 2.7.0-प्रीव्यू 1 के साथ tally

रूबी के नवीनतम संस्करण में एक नई विधि है tally। से रिलीज नोट्स :

Enumerable#tallyजोड़ दिया गया है। यह प्रत्येक तत्व की घटना को गिनता है।

["a", "b", "c", "b"].tally
#=> {"a"=>1, "b"=>2, "c"=>1}

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

ये रही पूरी बात:

k = ARGV.shift.to_i

pp ARGF
  .each_line
  .lazy
  .flat_map { @1.scan(/[A-Za-z]+/).map(&:downcase) }
  .tally
  .max_by(k, &:last)

संपादित करें: जोड़ा गया k कमांड लाइन तर्क के रूप में

इसके साथ चलाया जा सकता है ruby k filename.rb input.txt रूबी के 2.7.0-प्रीव्यू 1 संस्करण का उपयोग करके । यह रिलीज नोट्स पेज पर विभिन्न लिंक से डाउनलोड किया जा सकता है, या rbenv के साथ इंस्टॉल किया जा सकता हैrbenv install 2.7.0-dev

उदाहरण मेरे अपने बीट-अप, पुराने कंप्यूटर पर चलते हैं:

$ time ruby bentley.rb 10 ulysses64 
[["the", 968832],
 ["of", 528960],
 ["and", 466432],
 ["a", 421184],
 ["to", 322624],
 ["in", 320512],
 ["he", 270528],
 ["his", 213120],
 ["i", 191808],
 ["s", 182144]]

real    0m17.884s
user    0m17.720s
sys 0m0.142s

1
मैंने स्रोतों से रूबी को स्थापित किया। यह आपकी मशीन (15 सेकंड बनाम 17) पर जितनी तेजी से चलता है।
एंड्री मकुखा
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.