ความท้าทายในการเขียนโปรแกรมของเบนท์ลีย์: k คำที่พบบ่อยที่สุด


18

นี่อาจเป็นหนึ่งในความท้าทายการเข้ารหัสแบบคลาสสิกที่ได้รับการกำทอนในปี 1986 เมื่อคอลัมนิสต์จอนเบนท์ลีย์ขอให้โดนัลด์ Knuth เขียนโปรแกรมที่จะหาคำที่พบบ่อยที่สุดในไฟล์ Knuth ใช้วิธีแก้ปัญหาอย่างรวดเร็วโดยใช้แฮชลองในโปรแกรมที่มีความยาว 8 หน้าเพื่อแสดงให้เห็นถึงเทคนิคการเขียนโปรแกรมความรู้ของเขา Douglas McIlroy จาก Bell Labs วิพากษ์วิจารณ์การแก้ปัญหาของ Knuth ในขณะที่ไม่สามารถประมวลผลข้อความฉบับเต็มของคัมภีร์ไบเบิลและตอบกลับด้วยสายการบินเดียวที่ไม่รวดเร็วเท่านี้ แต่ทำงานให้เสร็จ:

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

ในปี 1987 บทความติดตามผลได้รับการตีพิมพ์พร้อมกับวิธีแก้ปัญหาอื่นซึ่งคราวนี้โดยศาสตราจารย์พรินซ์ตัน แต่มันก็ไม่สามารถแม้แต่ส่งคืนผลลัพธ์สำหรับคัมภีร์ไบเบิลเล่มเดียว!

คำอธิบายปัญหา

คำอธิบายปัญหาเดิม:

ให้ไฟล์ข้อความและเลขจำนวนเต็ม k คุณจะต้องพิมพ์คำที่พบบ่อยที่สุด k ในไฟล์ (และจำนวนที่เกิดขึ้น) ในการลดความถี่

การชี้แจงปัญหาเพิ่มเติม:

  • Knuth กำหนดคำเป็นสตริงของตัวอักษรละติน: [A-Za-z]+
  • อักขระอื่นทั้งหมดจะถูกละเว้น
  • ตัวพิมพ์ใหญ่และตัวพิมพ์เล็กถือว่าเทียบเท่า ( WoRd== word)
  • ไม่ จำกัด ขนาดไฟล์และความยาวของคำ
  • ระยะห่างระหว่างคำที่ต่อเนื่องกันอาจมีขนาดใหญ่โดยพล
  • โปรแกรมที่เร็วที่สุดคือโปรแกรมที่ใช้เวลา CPU ทั้งหมดอย่างน้อย (มัลติเธรดอาจไม่ช่วยได้)

กรณีทดสอบตัวอย่าง

ทดสอบ 1: Ulyssesโดย James Joyce ต่อกัน 64 ครั้ง (ไฟล์ 96 MB)

  • ดาวน์โหลดUlyssesจาก Project Gutenberg:wget http://www.gutenberg.org/files/4300/4300-0.txt
  • ทำการต่อ 64 ครั้ง: for i in {1..64}; do cat 4300-0.txt >> ulysses64; done
  • คำที่พบบ่อยที่สุดคือ "the" กับ 968832 ลักษณะที่ปรากฏ

ทดสอบ 2:ข้อความสุ่มที่สร้างขึ้นเป็นพิเศษgiganovel(ประมาณ 1 GB)

  • งูหลามสคริปต์ 3 เครื่องกำเนิดไฟฟ้าที่นี่
  • ข้อความประกอบด้วยคำศัพท์ที่แตกต่างกัน 148,391 คำซึ่งมีลักษณะคล้ายกับภาษาธรรมชาติ
  • คำที่พบบ่อยที่สุด:“ e” (11309 นัด) และ“ ihit” (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 "MacBook Pro พร้อมtimeคำสั่งUnix มาตรฐาน(" ผู้ใช้ ") หากเป็นไปได้โปรดใช้คอมไพเลอร์สมัยใหม่ (เช่นใช้รุ่น Haskell รุ่นล่าสุดไม่ใช่รุ่นเก่า)

อันดับนั้น

เวลารวมถึงโปรแกรมอ้างอิง:

                                              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 ดูเหมือนว่าจะเป็นนัย แต่ไม่ได้กล่าวอย่างชัดเจน
Post Rock Garf Hunter

@ SriotchilismO'Zaic ตอนนี้ใช่ แต่คุณไม่ควรพึ่งพากรณีทดสอบครั้งแรกเนื่องจากกรณีทดสอบที่ใหญ่กว่าอาจตามมา ulysses64 มีข้อเสียที่ชัดเจนของการซ้ำซ้อน: ไม่มีคำใหม่ปรากฏหลังจากไฟล์ 1/64 ดังนั้นจึงไม่ใช่กรณีทดสอบที่ดีมาก แต่ง่ายต่อการแจกจ่าย (หรือทำซ้ำ)
Andriy Makukha

3
ฉันหมายถึงกรณีทดสอบซ่อนเร้นที่คุณพูดถึงก่อนหน้านี้ หากคุณโพสต์แฮชตอนนี้เมื่อคุณเปิดเผยข้อความจริงเราสามารถมั่นใจได้ว่ามันยุติธรรมกับคำตอบและคุณไม่ได้เป็นกษัตริย์ แม้ว่าฉันคิดว่าแฮชสำหรับยูลิสซีนั้นมีประโยชน์บ้าง
โพสต์ร็อค Garf Hunter

1
@tsh นั่นคือความเข้าใจของฉัน: เช่นจะเป็นสองคำที่ e และ g
Moogie

1
@AndriyMakukha อ่าขอบคุณ นั่นเป็นเพียงข้อบกพร่อง ฉันแก้ไขพวกเขา
Anders Kaseorg

คำตอบ:


5

[ค]

การทำงานต่อไปนี้ใช้เวลาไม่เกิน 1.6 วินาทีสำหรับการทดสอบ 1ใน 2.8 Ghz Xeon W3530 ของฉัน สร้างโดยใช้ MinGW.org GCC-6.3.0-1 บน Windows 7:

ใช้สองอาร์กิวเมนต์เป็นอินพุต (พา ธ ไปยังไฟล์ข้อความและสำหรับจำนวน k ของคำที่ใช้บ่อยที่สุดในรายการ)

มันสร้างกิ่งไม้บนตัวอักษรของคำจากนั้นที่ตัวอักษรใบมันเพิ่มเคาน์เตอร์ จากนั้นตรวจสอบเพื่อดูว่าตัวนับใบไม้ปัจจุบันมากกว่าคำที่เล็กที่สุดที่พบบ่อยที่สุดในรายการคำที่พบบ่อยที่สุดหรือไม่ (ขนาดรายการคือจำนวนที่กำหนดผ่านอาร์กิวเมนต์บรรทัดคำสั่ง) หากเป็นเช่นนั้นเลื่อนระดับคำที่แสดงโดยตัวอักษรใบเป็นหนึ่งในบ่อยที่สุด ทั้งหมดนี้จะทำซ้ำจนกว่าจะไม่มีการอ่านตัวอักษรอีกต่อไปหลังจากนั้นรายการคำที่พบบ่อยที่สุดจะถูกส่งออกผ่านการทำซ้ำที่ไม่มีประสิทธิภาพในการค้นหาคำที่พบบ่อยที่สุดจากรายการของคำที่พบบ่อยที่สุด

ขณะนี้เป็นค่าเริ่มต้นที่จะส่งออกเวลาการประมวลผล แต่สำหรับวัตถุประสงค์ของความสอดคล้องกับการส่งอื่น ๆ ปิดการใช้งานการกำหนด TIMING ในรหัสที่มา

นอกจากนี้ฉันได้ส่งสิ่งนี้จากคอมพิวเตอร์ที่ทำงานและไม่สามารถดาวน์โหลดข้อความทดสอบ 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

ที่น่าประทับใจ! การใช้ list ทำให้มันเป็น O (Nk) ในกรณีที่แย่ที่สุดดังนั้นมันจะทำงานช้ากว่าโปรแกรม C ++ อ้างอิงสำหรับ giganovel ที่มี k = 100,000 แต่สำหรับ k << N มันเป็นผู้ชนะที่ชัดเจน
Andriy Makukha

1
@AndriyMakukha ขอบคุณ! ฉันรู้สึกประหลาดใจเล็กน้อยที่การใช้งานอย่างง่าย ๆ นั้นให้ความเร็วที่ยอดเยี่ยม ฉันสามารถทำให้ดีขึ้นสำหรับค่า k ที่มากขึ้นด้วยการเรียงลำดับรายการ (การเรียงลำดับไม่ควรแพงเกินไปเนื่องจากลำดับของรายการจะเปลี่ยนแปลงอย่างช้าๆ) แต่นั่นเพิ่มความซับซ้อนและอาจส่งผลกระทบต่อความเร็วสำหรับค่าที่ต่ำกว่าของ k จะมีการทดสอบ
Moogie

ใช่ฉันรู้สึกประหลาดใจเช่นกัน อาจเป็นเพราะโปรแกรมอ้างอิงใช้การเรียกใช้ฟังก์ชันจำนวนมากและคอมไพเลอร์ไม่สามารถปรับให้เหมาะสมอย่างเหมาะสม
Andriy Makukha

ประโยชน์ด้านประสิทธิภาพอื่นอาจมาจากการจัดสรรlettersอาเรย์ของ semistatic ในขณะที่การใช้การอ้างอิงจัดสรรโหนดต้นไม้แบบไดนามิก
Andriy Makukha

mmapไอเอ็นจีควรจะเร็ว (~ 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

สนิม

บนคอมพิวเตอร์ของฉันสิ่งนี้รัน giganovel 100000 เร็วขึ้น 42% (10.64 s เมื่อเทียบกับ 18.24 s) เมื่อเทียบกับวิธีแก้ปัญหาต้นไม้“ prefix tree + bins” ของ Moogie นอกจากนี้ยังไม่มีข้อ จำกัด ที่กำหนดไว้ล่วงหน้า (ต่างจากโซลูชัน 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
สุดยอด! ประสิทธิภาพที่ดีมากสำหรับการตั้งค่าทั้งสามนี้ ฉันอยู่ท่ามกลางการดูคำปราศรัยล่าสุดของแครอลนิโคลส์เกี่ยวกับสนิม :) ไวยากรณ์ผิดปกติเล็กน้อย แต่ฉันตื่นเต้นที่ได้เรียนรู้ภาษา: ดูเหมือนว่าจะเป็นภาษาเดียวในภาษาระบบหลัง C-C ++ เสียสละประสิทธิภาพมากในขณะที่ทำให้ชีวิตของนักพัฒนาง่ายขึ้นมาก
Andriy Makukha

เร็วมาก! ผมประทับใจ! ฉันสงสัยว่าตัวเลือกคอมไพเลอร์ที่ดีกว่าสำหรับ C (tree + bin) จะให้ผลลัพธ์ที่คล้ายกันหรือไม่
Moogie

@ Moogie ฉันทดสอบคุณ-O3แล้วและ-Ofastไม่สร้างความแตกต่างที่วัดได้
Anders Kaseorg

@Moogie gcc -O3 -march=native -mtune=native program.cผมได้รวบรวมรหัสของคุณเช่น
Andriy Makukha

@Andriy Makukha อา นั่นจะอธิบายความแตกต่างของความเร็วอย่างมากระหว่างผลลัพธ์ที่คุณได้รับเทียบกับผลลัพธ์ของฉัน: คุณใช้การตั้งค่าสถานะการปรับให้เหมาะสมแล้ว ฉันไม่คิดว่าจะมีการเพิ่มประสิทธิภาพโค้ดขนาดใหญ่จำนวนมากเหลืออยู่ ฉันไม่สามารถทดสอบการใช้แผนที่ตามคำแนะนำของผู้อื่นเนื่องจาก mingw ตายแล้วไม่มีการนำไปใช้ ... และจะเพิ่มขึ้น 5% เท่านั้น ฉันคิดว่าฉันจะต้องยอมจำนนต่อผลงานอันยอดเยี่ยมของ Anders ทำได้ดี!
Moogie

3

APL (Dyalog Unicode)

การทำงานต่อไปนี้ใช้เวลาไม่ถึง 8 วินาทีใน 2.6 Ghz i7-4720HQ ของฉันโดยใช้ Dyalog APL 17.0 แบบ 64 บิตบน Windows 10:

⎕{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

ดีมาก! มันเต้น Python export MAXWS=4096Mมันทำงานได้ดีที่สุดหลังจาก ฉันเดาว่ามันใช้ตารางแฮช เนื่องจากการลดขนาดพื้นที่ทำงานลงเหลือ 2 GB ทำให้มันช้าลงทั้ง 2 วินาที
Andriy Makukha

@AndriyMakukha ใช่ใช้ตารางแฮชตามนี้และฉันค่อนข้างแน่ใจว่าจะทำภายใน
อดัม

ทำไมมันเป็น O (N log N)? ดูเหมือนว่า Python (k คูณการกู้คืนกองคำที่ไม่ซ้ำกันทั้งหมด) หรือ AWK (การเรียงลำดับเฉพาะคำที่ไม่ซ้ำกัน) สำหรับฉัน ถ้าคุณไม่เรียงลำดับคำทั้งหมดเช่นในเชลล์สคริปต์ของ McIlroy ไม่ควรเป็น O (N log N)
Andriy Makukha

@AndriyMakukha มันเกรดทั้งหมด นี่คือสิ่งที่นักแสดงของเราเขียนถึงฉัน: ความซับซ้อนของเวลาคือ O (N log N) เว้นแต่คุณจะเชื่อในสิ่งที่น่าสงสัยในทางทฤษฎีเกี่ยวกับตารางแฮชซึ่งในกรณีนี้คือ O (N)
อดัม

เมื่อฉันเรียกใช้รหัสของคุณกับ 8, 16 และ 32 Ulysses มันจะช้าลงอย่างเป็นเส้นตรง บางทีนักแสดงของคุณอาจจำเป็นต้องพิจารณามุมมองของเขาอีกครั้งเกี่ยวกับความซับซ้อนของเวลาแฮชของตาราง :) นอกจากนี้โค้ดนี้ไม่สามารถใช้ได้กับกรณีทดสอบที่ใหญ่กว่า มันส่งคืนWS FULLแม้ว่าฉันจะเพิ่มพื้นที่ทำงานเป็น 6 GB
Andriy Makukha

2

[C] ต้นไม้คำนำหน้า + ถังขยะ

หมายเหตุ: คอมไพเลอร์ที่ใช้มีผลอย่างมากต่อความเร็วในการทำงานของโปรแกรม! ฉันใช้ gcc (MinGW.org GCC-8.2.0-3) 8.2.0 เมื่อใช้สวิตช์ -Ofastโปรแกรมจะทำงานเร็วกว่าโปรแกรมที่คอมไพล์ปกติเกือบ 50%

ความซับซ้อนของอัลกอริทึม

ฉันได้ตระหนักว่าการเรียงลำดับถังขยะที่ฉันแสดงเป็นรูปแบบของPigeonhostซึ่งหมายความว่าฉันสามารถรับความซับซ้อนของ Big O ของโซลูชันนี้ได้

ฉันคำนวณให้เป็น:

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) (เนื่องจากตัวอักษรแต่ละตัวถูกแมปโดยตรงกับโหนดและเรามักจะสำรวจระดับหนึ่งของต้นไม้สำหรับแต่ละตัวอักษรเสมอ)

Pigeon Hole sorting คือ O (N + n) โดยที่ n คือช่วงของค่าคีย์อย่างไรก็ตามสำหรับปัญหานี้เราไม่จำเป็นต้องเรียงลำดับค่าทั้งหมดเฉพาะตัวเลข k เท่านั้นดังนั้นกรณีที่แย่ที่สุดคือ O (N + k)

การรวมกันให้ผลผลิต O (1 + N + k)

ความซับซ้อนของพื้นที่สำหรับการก่อสร้างต้นไม้นั้นเกิดจากข้อเท็จจริงที่ว่ากรณีที่แย่ที่สุดคือโหนด 26 * M หากข้อมูลประกอบด้วยคำเดียวที่มีจำนวนตัวอักษร M และแต่ละโหนดมี 26 โหนด (เช่นสำหรับตัวอักษรของตัวอักษร) ดังนั้น O (26 * M) = O (M)

สำหรับการเรียง Pigeon Hole มีความซับซ้อนของพื้นที่ O (N + n)

การรวมกันให้ผลผลิต O (26 * M + N + n) = O (M + N + n)

ขั้นตอนวิธี

ใช้สองอาร์กิวเมนต์เป็นอินพุต (พา ธ ไปยังไฟล์ข้อความและสำหรับจำนวน k ของคำที่ใช้บ่อยที่สุดในรายการ)

จากรายการอื่น ๆ ของฉันเวอร์ชั่นนี้มีทางลาดที่มีเวลาน้อยมากและค่า k เพิ่มขึ้นเมื่อเทียบกับโซลูชันอื่น ๆ แต่จะช้ากว่าอย่างเห็นได้ชัดสำหรับค่า k ต่ำ แต่ควรเร็วกว่ามากสำหรับค่า k ที่ใหญ่กว่า

มันสร้างกิ่งต้นไม้บนตัวอักษรของคำจากนั้นที่ตัวอักษรใบมันเพิ่มเคาน์เตอร์ จากนั้นเพิ่มคำลงในถังขยะของคำที่มีขนาดเท่ากัน (หลังจากลบคำนั้นออกจากถังคำที่มีอยู่แล้ว) ทั้งหมดนี้จะทำซ้ำจนกว่าจะไม่มีการอ่านตัวอักษรอีกต่อไปหลังจากนั้นถังขยะจะถูกทำซ้ำย้อนกลับ k คูณเริ่มต้นจากถังที่ใหญ่ที่สุดและคำของถังแต่ละถังจะถูกส่งออก

ขณะนี้เป็นค่าเริ่มต้นที่จะส่งออกเวลาการประมวลผล แต่สำหรับวัตถุประสงค์ของความสอดคล้องกับการส่งอื่น ๆ ปิดการใช้งานการกำหนด TIMING ในรหัสที่มา

// 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: ตอนนี้ใช้ตัวชี้เลขคณิตแทนการเข้าถึงอาร์เรย์สำหรับการเพิ่มประสิทธิภาพความเร็ว


ว้าว! 100,000 คำที่พบบ่อยที่สุดจากไฟล์ 1 GB ใน 11 วินาที ... นี่มันช่างเวทย์มนตร์
Andriy Makukha

ไม่มีเคล็ดลับ ... เพียงแลกเปลี่ยนเวลา CPU เพื่อการใช้งานหน่วยความจำอย่างมีประสิทธิภาพ ฉันประหลาดใจกับผลลัพธ์ของคุณ ... ในพีซีรุ่นเก่าของฉันมันใช้เวลากว่า 60 วินาที ฉันสังเกตเห็นว่า II กำลังทำการเปรียบเทียบที่ไม่จำเป็นและสามารถเลื่อนการอ้างอิงจนกว่าไฟล์นั้นจะถูกประมวลผล มันควรทำให้มันเร็วขึ้น ฉันจะลองเร็ว ๆ นี้และอัปเดตคำตอบของฉัน
Moogie

@AndriyMakukha ฉันได้เลื่อนการเติมข้อมูลลงในถังขยะจนกระทั่งหลังจากคำทั้งหมดได้รับการประมวลผลและต้นไม้ถูกสร้างขึ้น สิ่งนี้หลีกเลี่ยงการเปรียบเทียบที่ไม่จำเป็นและการจัดการองค์ประกอบถังขยะ ฉันได้เปลี่ยนวิธีการสร้างผลลัพธ์เมื่อฉันพบว่าการพิมพ์ใช้เวลานานพอสมควร!
Moogie

ในเครื่องของฉันอัพเดตนี้ไม่ได้สร้างความแตกต่างที่เห็นได้ชัดเจน อย่างไรก็ตามมันทำงานได้เร็วมากในulysses64ครั้งเดียวดังนั้นจึงเป็นผู้นำคนปัจจุบัน
Andriy Makukha

จะต้องเป็นปัญหาที่ไม่ซ้ำกับเครื่องคอมพิวเตอร์ของฉันแล้ว :) ผมสังเกตเห็นความเร็วที่ 5 ที่สองขึ้นเมื่อใช้นี้ขั้นตอนวิธีการส่งออกใหม่
Moogie

2

J

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

ไม่มีข้อ จำกัด ยกเว้นจำนวนหน่วยความจำระบบที่มีอยู่


เร็วมากสำหรับเคสทดสอบขนาดเล็ก! ดี! อย่างไรก็ตามสำหรับคำที่มีขนาดใหญ่โดยพลการมันจะตัดคำในผลลัพธ์ ฉันไม่แน่ใจว่ามีการ จำกัด จำนวนอักขระในคำหรือถ้ามันเป็นเพียงเพื่อให้ออกรัดกุมมากขึ้น
Andriy Makukha

@AndriyMakukha ใช่...เกิดขึ้นเนื่องจากมีการตัดทอนผลลัพธ์ต่อบรรทัด ฉันเพิ่มหนึ่งบรรทัดที่จุดเริ่มต้นเพื่อปิดการตัดทั้งหมด มันช้าลงเมื่อใช้ giganovel เนื่องจากมันใช้หน่วยความจำมากกว่าเดิมเนื่องจากมีคำที่ไม่เหมือนใคร
ระยะทาง

ที่ดี! ตอนนี้มันผ่านการทดสอบทั่วไป และมันก็ไม่ได้ช้าลงบนเครื่องของฉัน ในความเป็นจริงมีการเร่งความเร็วเล็กน้อย
Andriy Makukha

2

C ++ (a la Knuth)

ฉันสงสัยว่าโปรแกรมของ Knuth จะเดินทางอย่างไรฉันจึงแปลโปรแกรม (เดิมคือ Pascal) เป็น C ++

แม้ว่าเป้าหมายหลักของ Knuth นั้นไม่ใช่ความเร็ว แต่เพื่อแสดงให้เห็นถึงระบบ WEB ของการเขียนโปรแกรมความรู้โปรแกรมนั้นมีการแข่งขันที่น่าประหลาดใจและนำไปสู่การแก้ปัญหาที่รวดเร็วกว่าคำตอบใด ๆ ที่นี่ นี่คือคำแปลของโปรแกรมของฉัน (หมายเลข "ส่วน" ที่สอดคล้องกันของโปรแกรม 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;
}

ความแตกต่างจากโปรแกรมของ Knuth:

  • ฉันรวม Knuth 4 อาร์เรย์link, sibling, countและchเป็น array ของหนึ่งstruct Node (พบว่ามันง่ายต่อการเข้าใจวิธีนี้)
  • ฉันเปลี่ยนการแปลงข้อความเป็นส่วน ๆ ของวรรณกรรม (สไตล์เว็บ) เป็นการเรียกฟังก์ชั่นทั่วไป (และมาโครสองสามตัว)
  • เราไม่จำเป็นต้องใช้ข้อตกลง / ข้อ จำกัด I / O แปลก ๆ ของ Pascal ดังนั้นการใช้freadและdata[i] | 32 - 'a'เหมือนในคำตอบอื่น ๆ ที่นี่แทนที่จะเป็นวิธีแก้ปัญหาของ Pascal
  • ในกรณีที่เราเกินขีด จำกัด (หมดพื้นที่) ในขณะที่โปรแกรมกำลังทำงานอยู่โปรแกรมดั้งเดิมของ Knuth จะจัดการกับมันอย่างสง่างามโดยการวางคำต่อมาและพิมพ์ข้อความที่ท้าย (มันค่อนข้างไม่ถูกต้องที่จะบอกว่า McIlroy "วิพากษ์วิจารณ์การแก้ปัญหาของ Knuth ที่ไม่สามารถประมวลผลข้อความทั้งหมดในพระคัมภีร์" เขาเพียงชี้ให้เห็นว่าบางครั้งคำที่พบบ่อยอาจเกิดขึ้นช้ามากในข้อความเช่นคำว่า "Jesus "ในพระคัมภีร์ดังนั้นเงื่อนไขข้อผิดพลาดจึงไม่เป็นพิษเป็นภัยเลย) ฉันใช้วิธีที่น่าสนใจ (และง่ายกว่า) ในการยุติโปรแกรม
  • โปรแกรมประกาศ TRIE_SIZE คงที่เพื่อควบคุมการใช้หน่วยความจำซึ่งฉันชน (ค่าคงที่ของ 32767 นั้นถูกเลือกสำหรับความต้องการดั้งเดิม - "ผู้ใช้ควรจะสามารถค้นหาคำที่พบบ่อยที่สุด 100 คำในกระดาษยี่สิบหน้าทางเทคนิค (ประมาณไฟล์ 50K ไบต์)" และเนื่องจาก Pascal จัดการได้ดีกับจำนวนเต็มในช่วง ประเภทและแพ็คให้เหมาะสมที่สุดเราต้องเพิ่ม 25x เป็น 800,000 เนื่องจากอินพุตทดสอบตอนนี้ใหญ่ขึ้น 20 ล้านเท่า)
  • สำหรับการพิมพ์ครั้งสุดท้ายของสตริงเราก็สามารถเดินทั้งคู่และทำสตริงที่โง่ (อาจจะเป็นกำลังสอง) ผนวก

นอกเหนือจากนั้นนี่เป็นโปรแกรมของ Knuth (โดยใช้โครงสร้างข้อมูลแบบแฮช trie / ที่บรรจุแบบ trie และที่จัดเรียงกลุ่ม) และทำงานแบบเดียวกัน (เหมือนโปรแกรม Pascal ของ Knuth) ในขณะที่วนรอบอักขระทั้งหมดในอินพุต โปรดทราบว่าไม่มีการใช้อัลกอริธึมภายนอกหรือไลบรารีโครงสร้างข้อมูลและคำที่มีความถี่เท่ากันจะถูกพิมพ์ตามลำดับตัวอักษร

การจับเวลา

รวบรวมด้วย

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

เมื่อรันบน testcase ที่ใหญ่ที่สุดที่นี่ ( giganovelโดยมี 100,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...]

(บรรทัดบนสุดคือโซลูชัน Rust ของ Anders Kaseorg; ด้านล่างเป็นโปรแกรมด้านบนนี่คือการจับเวลาจากการวิ่ง 100 ครั้งด้วยค่าเฉลี่ย, นาที, สูงสุด, ค่ามัธยฐานและควอไทล์)

การวิเคราะห์

ทำไมถึงเร็วกว่านี้? ไม่ใช่ว่า C ++ เร็วกว่า Rust หรือโปรแกรมของ Knuth นั้นเร็วที่สุด - อันที่จริงแล้วโปรแกรมของ Knuth นั้นช้าลงในการแทรก เหตุผลที่ฉันสงสัยว่าเกี่ยวข้องกับสิ่งที่ Knuth บ่นเกี่ยวกับในปี 2008 :

Flame เกี่ยวกับพอยน์เตอร์ 64- บิต

เป็นเรื่องงี่เง่าที่มีตัวชี้ 64- บิตเมื่อฉันรวบรวมโปรแกรมที่ใช้ RAM น้อยกว่า 4 กิกะไบต์ เมื่อค่าตัวชี้ดังกล่าวปรากฏขึ้นภายในโครงสร้างพวกเขาไม่เพียง แต่เสียหน่วยความจำเพียงครึ่งเดียว แต่ยังทิ้งครึ่งหนึ่งของแคชอย่างมีประสิทธิภาพ

โปรแกรมข้างต้นใช้ดัชนีอาร์เรย์แบบ 32 บิต (ไม่ใช่ตัวชี้แบบ 64- บิต) ดังนั้นโครงสร้าง "โหนด" จึงใช้หน่วยความจำน้อยกว่าดังนั้นจึงมีโหนดจำนวนมากบนสแต็กและมีแคชน้อยกว่า (อันที่จริงมีงานบางอย่างเกี่ยวกับเรื่องนี้เป็นx32 ABIแต่ดูเหมือนว่าจะไม่อยู่ในสภาพที่ดีแม้ว่าความคิดจะมีประโยชน์ชัดเจนเช่นดูการประกาศล่าสุดเกี่ยวกับการบีบอัดตัวชี้ใน V8ไม่เป็นไร) giganovelโปรแกรมนี้ใช้ 12.8 MB สำหรับ trie (บรรจุ) เทียบกับ Rust ของ 32.18MB สำหรับ trie (บน)giganovel ) เราสามารถขยายขนาด 1,000 เท่า (จาก "giganovel" ถึง "teranovel" พูด) และยังคงไม่เกินดัชนี 32 บิตดังนั้นนี่จึงเป็นตัวเลือกที่สมเหตุสมผล

ตัวแปรที่เร็วกว่า

เราสามารถปรับให้เหมาะสมสำหรับความเร็วและนำหน้าการบรรจุดังนั้นเราจึงสามารถใช้ trie (ไม่บรรจุ) เช่นเดียวกับใน Rust โดยมีดัชนีแทนพอยน์เตอร์ สิ่งนี้ให้สิ่งที่เร็วกว่าและไม่มีการ จำกัด จำนวนคำที่แตกต่างอักขระ ฯลฯ :

#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 สำหรับ trie ของมันและจัดการให้เร็วขึ้น การจับเวลาของโปรแกรมนี้ (บรรทัดสุดท้าย) เปรียบเทียบกับการกำหนดเวลาก่อนหน้าที่กล่าวถึง:

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...]

ฉันอยากรู้ว่าโปรแกรมนี้ (หรือโปรแกรมแฮช - ตี้) ต้องการแปลเป็น Rust หรือไม่หรือไม่ :-)

รายละเอียดเพิ่มเติม

  1. เกี่ยวกับโครงสร้างข้อมูลที่ใช้ที่นี่: คำอธิบายของความพยายาม "การบรรจุ" ได้รับในการออกกำลังกาย 4 ของมาตรา 6.3 (การค้นหาแบบดิจิตอล, พยายาม) ในเล่ม 3 ของ TAOCP และในวิทยานิพนธ์ของนักเรียนของ Knuth Frank เหลียงเกี่ยวกับการใส่ยัติภังค์ใน TeX : โปรแกรม Word Hy-phen-a-tion โดยดอทคอมใส่เอ้อ

  2. บริบทของคอลัมน์ของเบนท์ลีย์โปรแกรมของ Knuth และการทบทวนของ McIlroy (เพียงส่วนเล็ก ๆ ซึ่งเกี่ยวกับปรัชญาของ Unix) นั้นชัดเจนขึ้นในแง่ของคอลัมน์ก่อนหน้าและหลังและประสบการณ์ก่อนหน้าของ Knuth รวมถึงผู้เรียบเรียง TAOCP และ TeX

  3. มี แบบฝึกหัดหนังสือทั้งเล่มในสไตล์การเขียนโปรแกรมแสดงวิธีการต่าง ๆ ของโปรแกรมนี้ ฯลฯ

ฉันมีโพสต์บล็อกที่ยังไม่เสร็จอธิบายรายละเอียดในประเด็นข้างต้น อาจแก้ไขคำตอบนี้เมื่อทำเสร็จแล้ว ในขณะเดียวกันโพสต์คำตอบนี้ที่นี่ต่อไปในโอกาส (10 ม.ค. ) วันเกิดของ Knuth :-)


! น่ากลัว ในที่สุดไม่เพียง แต่มีคนโพสต์โซลูชันของ Knuth (ฉันตั้งใจจะทำ แต่ใน Pascal) ด้วยการวิเคราะห์ที่ยอดเยี่ยมและประสิทธิภาพที่ทำให้การโพสต์ก่อนหน้านี้ดีที่สุด แต่ยังสร้างสถิติใหม่สำหรับความเร็วด้วยโปรแกรม C ++ อีกด้วย! ยอดเยี่ยม
Andriy Makukha

ความคิดเห็นสองข้อที่ฉันมี: 1) โปรแกรมที่สองของคุณล้มเหลวในขณะนี้Segmentation fault: 11สำหรับกรณีทดสอบที่มีคำและช่องว่างขนาดใหญ่โดยพลการ; 2) แม้ว่ามันอาจรู้สึกว่าฉันเห็นใจ "คำวิจารณ์" ของ McIlroy แต่ฉันก็ตระหนักดีว่าความตั้งใจของ Knuth นั้นเป็นเพียงการแสดงเทคนิคการเขียนโปรแกรมความรู้ของเขาในขณะที่ McIlroy วิจารณ์จากมุมมองทางวิศวกรรม McIlroy ตัวเองยอมรับในภายหลังว่ามันไม่ได้เป็นสิ่งที่ยุติธรรมที่จะทำ
Andriy Makukha

@AndriyMakukha โอ้อุ๊ยนั่นคือการเรียกซ้ำword_for; ซ่อมมันตอนนี้ ใช่ McIlroy ในฐานะผู้ประดิษฐ์ท่อ Unix ใช้โอกาสในการประกาศข่าวประเสริฐปรัชญาของUnixในการเขียนเครื่องมือขนาดเล็ก มันเป็นปรัชญาที่ดีเมื่อเทียบกับ Knuth ที่น่าผิดหวัง (ถ้าคุณกำลังพยายามอ่านโปรแกรมของเขา) วิธีการแบบเสาหิน แต่ในบริบทมันค่อนข้างไม่ยุติธรรมและด้วยเหตุผลอื่น: วันนี้ Unix มีให้บริการอย่างกว้างขวาง แต่ในปี 1986 ถูกกักตัว ถึง Bell Labs, Berkeley, ฯลฯ ("บริษัท ของเขาสร้าง prefab ที่ดีที่สุดในธุรกิจ")
ShreevatsaR

Works! ขอแสดงความยินดีกับกษัตริย์องค์ใหม่ :-P สำหรับ Unix และ Knuth เขาดูเหมือนจะไม่ชอบระบบมากนักเพราะมีและเป็นเอกภาพเล็กน้อยระหว่างเครื่องมือต่าง ๆ (เช่นเครื่องมือมากมายกำหนด regexes แตกต่างกัน)
Andriy Makukha

1

Python 3

การติดตั้งด้วยพจนานุกรมง่าย ๆ นี้เร็วกว่าการใช้พจนานุกรม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
ฉันสามารถทดสอบกับ giganovel บนระบบของฉันเท่านั้นและใช้เวลาค่อนข้างนาน (~ 90sec) gutenbergproject ถูกบล็อกในเยอรมนีด้วยเหตุผลทางกฎหมาย ...
movatica

น่าสนใจ มันheapqไม่ได้เพิ่มประสิทธิภาพใด ๆ ให้กับCounter.most_commonวิธีการหรือenumerate(sorted(...))ใช้heapqภายใน
Andriy Makukha

ฉันทดสอบด้วย Python 2 และประสิทธิภาพก็คล้ายกันดังนั้นฉันเดาว่าการเรียงลำดับใช้งานได้เร็วเท่าที่Counter.most_commonควร
Andriy Makukha

ใช่บางทีมันก็แค่กระวนกระวายใจในระบบของฉัน ... อย่างน้อยก็ไม่ช้าลง :) แต่การค้นหา regex เร็วกว่าการวนซ้ำอักขระ ดูเหมือนว่าจะมีการใช้งานนักแสดงค่อนข้าง
movatica

1

[C] ต้นไม้คำนำหน้า + รายการลิงก์ที่เรียงลำดับ

ใช้สองอาร์กิวเมนต์เป็นอินพุต (พา ธ ไปยังไฟล์ข้อความและสำหรับจำนวน k ของคำที่ใช้บ่อยที่สุดในรายการ)

จากรายการอื่น ๆ ของฉันเวอร์ชั่นนี้เร็วกว่ามากสำหรับค่า k ที่มากขึ้น แต่ด้วยค่าใช้จ่ายประสิทธิภาพเล็กน้อยที่ค่า k ที่ต่ำกว่า

มันสร้างกิ่งต้นไม้บนตัวอักษรของคำจากนั้นที่ตัวอักษรใบมันเพิ่มเคาน์เตอร์ จากนั้นตรวจสอบเพื่อดูว่าตัวนับ leaf ปัจจุบันมากกว่าคำที่เล็กที่สุดที่พบบ่อยที่สุดในรายการคำที่พบบ่อยที่สุดหรือไม่ (ขนาดรายการคือจำนวนที่กำหนดผ่านอาร์กิวเมนต์บรรทัดคำสั่ง) หากเป็นเช่นนั้นเลื่อนระดับคำที่แสดงโดยตัวอักษรใบเป็นหนึ่งในบ่อยที่สุด หากมีคำที่พบบ่อยที่สุดอยู่แล้วให้สลับกับคำที่พบบ่อยที่สุดต่อไปหากจำนวนคำนั้นสูงขึ้นดังนั้นจึงเรียงลำดับรายการไว้ ทั้งหมดนี้ทำซ้ำจนกว่าจะไม่มีการอ่านตัวอักษรอีกต่อไปหลังจากนั้นรายการคำที่พบบ่อยที่สุดจะถูกส่งออก

ขณะนี้เป็นค่าเริ่มต้นที่จะส่งออกเวลาการประมวลผล แต่สำหรับวัตถุประสงค์ของความสอดคล้องกับการส่งอื่น ๆ ปิดการใช้งานการกำหนด TIMING ในรหัสที่มา

// 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 = 12 eroilk 111 iennoa 10 yttelen 110 engyt100,000:
Andriy Makukha

ฉันคิดว่าฉันมีความคิดเกี่ยวกับเหตุผล ความคิดของฉันคือฉันจะต้องทำซ้ำสลับคำในรายการเมื่อตรวจสอบว่าคำปัจจุบันของคำสูงสุดถัดไป เมื่อฉันมีเวลาฉันจะตรวจสอบ
Moogie

อืมดูเหมือนว่าการแก้ไขอย่างง่าย ๆ ในการเปลี่ยน if เป็นขณะทำงาน แต่มันก็ช้าลงอย่างมากสำหรับอัลกอริทึมสำหรับค่าที่มากขึ้นของ k ฉันอาจต้องคิดถึงวิธีแก้ปัญหาที่ฉลาดกว่านี้
Moogie

1

ค#

หนึ่งนี้ควรจะทำงานกับล่าสุด.net SDKs

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 คำ
Andriy Makukha

@AndriyMakukha: ฉันเห็นว่าฉันยังนับคำนำหน้าคำที่ไม่เคยเกิดขึ้นในการนับครั้งสุดท้าย ฉันหลีกเลี่ยงการเขียนเอาต์พุตทั้งหมดเนื่องจากเอาต์พุตคอนโซลค่อนข้างช้าใน windows ฉันสามารถเขียนผลลัพธ์ไปยังไฟล์ได้หรือไม่?
เรียกซ้ำ

กรุณาพิมพ์มันออกมาตรฐานกรุณา สำหรับ k = 10 ควรรวดเร็วในทุกเครื่อง คุณยังสามารถเปลี่ยนเส้นทางเอาต์พุตไปยังไฟล์จากบรรทัดคำสั่ง เช่นนี้
Andriy Makukha

@AndriyMakukha: ฉันเชื่อว่าฉันได้แก้ไขปัญหาทั้งหมดแล้ว ฉันพบวิธีสร้างผลลัพธ์ที่ต้องการทั้งหมดโดยไม่ต้องเสียค่าใช้จ่ายมาก
เรียกซ้ำ

ผลลัพธ์นี้ยอดเยี่ยมมาก! ดีมาก. ฉันปรับเปลี่ยนโปรแกรมของคุณให้พิมพ์จำนวนความถี่เช่นเดียวกับวิธีแก้ปัญหาอื่น ๆ
Andriy Makukha

1

Ruby 2.7.0-preview1 กับ 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-preview1 ของ Ruby สามารถดาวน์โหลดได้จากลิงค์ต่าง ๆ ในหน้าบันทึกประจำรุ่นหรือติดตั้งโดยใช้ rbenvrbenv 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
ฉันติดตั้ง Ruby จากแหล่งต่าง ๆ มันทำงานเร็วพอ ๆ กับบนเครื่องของคุณ (15 วินาทีกับ 17 วินาที)
Andriy Makukha
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.