Thử thách mã hóa của Bentley: k từ thường xuyên nhất


17

Đây có lẽ là một trong những thách thức mã hóa cổ điển gây được tiếng vang vào năm 1986, khi chuyên mục Jon Bentley yêu cầu Donald Knuth viết một chương trình sẽ tìm thấy k từ thường xuyên nhất trong một tệp. Knuth đã triển khai một giải pháp nhanh bằng cách sử dụng hàm băm trong một chương trình dài 8 trang để minh họa cho kỹ thuật lập trình biết chữ của mình. Douglas McIlroy của Bell Labs đã chỉ trích giải pháp của Knuth là thậm chí không thể xử lý toàn bộ Kinh thánh và trả lời bằng một bản tóm tắt, điều đó không nhanh chóng, nhưng đã hoàn thành công việc:

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

Năm 1987, một bài báo tiếp theo đã được xuất bản với một giải pháp khác, lần này là bởi một giáo sư Princeton. Nhưng nó thậm chí không thể trả lại kết quả cho một Kinh thánh!

Mô tả vấn đề

Mô tả vấn đề gốc:

Cho một tệp văn bản và một số nguyên k, bạn phải in k từ phổ biến nhất trong tệp (và số lần xuất hiện của chúng) với tần suất giảm.

Làm rõ vấn đề bổ sung:

  • Knuth định nghĩa một từ là một chuỗi các chữ cái Latinh: [A-Za-z]+
  • tất cả các nhân vật khác bị bỏ qua
  • chữ hoa và chữ thường được coi là tương đương ( WoRd== word)
  • không giới hạn kích thước tập tin cũng như độ dài từ
  • khoảng cách giữa các từ liên tiếp có thể lớn tùy ý
  • chương trình nhanh nhất là chương trình sử dụng tổng thời gian CPU ít nhất (đa luồng có thể sẽ không giúp ích)

Các trường hợp thử nghiệm mẫu

Bài kiểm tra 1: Ulysses của James Joyce đã ghép nối 64 lần (tệp 96 MB).

  • Tải xuống Ulysses từ Project Gutenberg:wget http://www.gutenberg.org/files/4300/4300-0.txt
  • Nối nó 64 lần: for i in {1..64}; do cat 4300-0.txt >> ulysses64; done
  • Từ thường gặp nhất là những người nổi tiếng với 968832 lần xuất hiện.

Kiểm tra 2: Văn bản ngẫu nhiên được tạo đặc biệt giganovel(khoảng 1 GB).

  • Kịch bản trình tạo Python 3 tại đây .
  • Văn bản chứa 148391 từ riêng biệt xuất hiện tương tự như ngôn ngữ tự nhiên.
  • Những từ thường gặp nhất: sự xuất hiện của 11 tuổi (11309 lần xuất hiện) và sự xuất hiện của tôi (11290 lần xuất hiện).

Kiểm tra tổng quát: các từ lớn tùy ý với các khoảng trống lớn tùy ý.

Tham khảo thực hiện

Sau khi xem xét Rosetta Code cho vấn đề này và nhận ra rằng nhiều triển khai rất chậm (chậm hơn so với kịch bản shell!), Tôi đã thử nghiệm một vài triển khai tốt ở đây . Dưới đây là hiệu suất cho ulysses64cùng với độ phức tạp thời gian:

                                     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)

Bạn có thể đánh bại nó?

Kiểm tra

Hiệu suất sẽ được đánh giá bằng MacBook Pro 13 "2017 với timelệnh Unix tiêu chuẩn (thời gian" người dùng "). Nếu có thể, vui lòng sử dụng trình biên dịch hiện đại (ví dụ: sử dụng phiên bản Haskell mới nhất, không phải phiên bản kế thừa).

Thứ hạng cho đến nay

Thời gian, bao gồm các chương trình tham khảo:

                                              k=10                  k=100K
                                     ulysses64      giganovel      giganovel
C (trie + bins) by Moogie            0.704          9.568          9.459
C (trie + list) by Moogie            0.767          6.051          82.306
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

Xếp hạng tích lũy * (%, điểm tốt nhất có thể - 300):

#     Program                         Score  Generality
 1  Rust (trie) by Anders Kaseorg       334     Yes
 2  C (trie + bins) by Moogie           384      x
 3  J by miles                          852     Yes
 4  C# (trie) by recursive             1278      x
 5  C (trie + list) by Moogie          1306      x
 6  C++ (trie + heap)                  2255      x
 7  Python (dict) by movatica          4316     Yes
 8  Python (Counter)                   4583     Yes
 9  Ruby (tally) by daniero            7264     Yes
10  AWK + sort                         9422     Yes
11  McIlroy (tr + sort + uniq)        28014     Yes

* Tổng hiệu suất thời gian liên quan đến các chương trình tốt nhất trong mỗi ba bài kiểm tra.

Chương trình tốt nhất: tại đây .


Điểm số chỉ là thời gian trên Ulysses? Có vẻ như ngụ ý nhưng nó không được nói rõ ràng
Wheat Wizard

@ SriotchilismO'Z cổ, bây giờ, có. Nhưng bạn không nên dựa vào trường hợp thử nghiệm đầu tiên vì các trường hợp thử nghiệm lớn hơn có thể xảy ra. ulyskes64 có nhược điểm rõ ràng là lặp đi lặp lại: không có từ mới nào xuất hiện sau 1/64 của tệp. Vì vậy, nó không phải là một trường hợp thử nghiệm rất tốt, nhưng nó rất dễ phân phối (hoặc sao chép).
Andriy Makukha

3
Tôi có nghĩa là các trường hợp thử nghiệm ẩn mà bạn đã nói về trước đó. Nếu bạn đăng băm ngay bây giờ khi bạn tiết lộ các văn bản thực tế, chúng tôi có thể đảm bảo rằng nó công bằng với các câu trả lời và bạn không phải là vua. Mặc dù tôi cho rằng hàm băm cho Ulysses có phần hữu ích.
Phù thủy lúa mì

1
@tsh Đó là sự hiểu biết của tôi: ví dụ: sẽ là hai từ e và g
Moogie

1
@AndriyMakukha À, cảm ơn. Đó chỉ là những con bọ; Tôi đã sửa chúng.
Anders Kaseorg

Câu trả lời:


4

[C]

Phần sau chạy trong dưới 1,6 giây cho Bài kiểm tra 1 trên Xeon W3530 2,8 Ghz của tôi. Được xây dựng bằng MinGW.org GCC-6.3.0-1 trên Windows 7:

Phải mất hai đối số làm đầu vào (đường dẫn đến tệp văn bản và cho k số từ thường xuyên nhất để liệt kê)

Nó chỉ đơn giản là tạo một nhánh cây trên các chữ cái, sau đó tại các chữ cái lá, nó tăng một bộ đếm. Sau đó kiểm tra xem bộ đếm lá hiện tại có lớn hơn từ thường xuyên nhất nhỏ nhất trong danh sách các từ thường xuyên nhất không. (kích thước danh sách là số được xác định thông qua đối số dòng lệnh) Nếu vậy thì hãy quảng bá từ được biểu thị bằng chữ cái lá là một trong những từ thường xuyên nhất. Tất cả điều này lặp lại cho đến khi không còn chữ nào được đọc. Sau đó, danh sách các từ thường xuyên nhất được xuất ra thông qua tìm kiếm lặp không hiệu quả cho từ thường xuyên nhất từ ​​danh sách các từ thường xuyên nhất.

Hiện tại nó mặc định để xuất thời gian xử lý, nhưng với mục đích thống nhất với các lần gửi khác, hãy vô hiệu hóa định nghĩa TIMING trong mã nguồn.

Ngoài ra, tôi đã gửi cái này từ máy tính làm việc và không thể tải xuống văn bản Test 2. Nó sẽ hoạt động với Thử nghiệm 2 này mà không cần sửa đổi, tuy nhiên giá trị MAX_LETTER_INSTANCES có thể cần phải tăng lê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

// 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;
}

Đối với Bài kiểm tra 1 và trong 10 từ thường xuyên nhất và với thời gian được bật, nó sẽ in:

 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

Ấn tượng! Việc sử dụng danh sách được cho là làm cho nó O (Nk) trong trường hợp xấu nhất, vì vậy nó chạy chậm hơn chương trình C ++ tham chiếu cho gigan xẻ với k = 100.000. Nhưng với k << N thì đó là một người chiến thắng rõ ràng.
Andriy Makukha

1
@AndriyMakukha Cảm ơn! Tôi hơi ngạc nhiên khi việc thực hiện đơn giản như vậy mang lại tốc độ lớn. Tôi có thể làm cho nó tốt hơn cho các giá trị lớn hơn của k bằng cách sắp xếp danh sách. (việc sắp xếp không nên quá đắt vì thứ tự danh sách sẽ thay đổi chậm) nhưng điều đó làm tăng thêm độ phức tạp và có thể sẽ ảnh hưởng đến tốc độ của các giá trị k thấp hơn. Sẽ phải thử nghiệm
Moogie

Vâng, tôi cũng ngạc nhiên. Có thể là do chương trình tham chiếu sử dụng rất nhiều lệnh gọi hàm và trình biên dịch không tối ưu hóa nó đúng cách.
Andriy Makukha

Một lợi ích hiệu năng khác có lẽ đến từ việc phân bổ lettersmảng theo ngữ nghĩa, trong khi việc thực hiện tham chiếu phân bổ các nút cây một cách linh hoạt.
Andriy Makukha

mmap-ing nên nhanh hơn (~ 5% trên laptop linux của tôi): #include<sys/mman.h>, <sys/stat.h>, <fcntl.h>, thay thế tập tin đọc với int d=open(argv[1],0);struct stat s;fstat(d,&s);dataLength=s.st_size;data=mmap(0,dataLength,1,1,d,0);và bình luận rafree(data);
NGN

3

APL (Unicode Dy)

Phần sau chạy dưới 8 giây trên 2.6 Ghz i7-4720HQ của tôi bằng cách sử dụng Dyalog APL 17.0 64 bit trên Windows 10:

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

Đầu tiên nó nhắc tên tập tin, sau đó cho k. Lưu ý rằng một phần đáng kể của thời gian chạy (khoảng 1 giây) chỉ là đọc tệp trong.

Theo thời gian, bạn sẽ có thể chuyển những điều sau đây vào dyalogtệp thực thi của mình (đối với mười từ thường xuyên nhất):

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

Nó nên in:

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

Rất đẹp! Nó đánh bại Python. Nó hoạt động tốt nhất sau export MAXWS=4096M. Tôi đoán, nó sử dụng bảng băm? Bởi vì việc giảm kích thước không gian làm việc xuống 2 GB khiến nó chậm hơn toàn bộ 2 giây.
Andriy Makukha

@AndriyMakukha Có, sử dụng bảng băm theo điều này và tôi khá chắc chắn rằng bên trong cũng vậy.
Adám

Tại sao lại là O (N log N)? Trông giống như Python (k lần khôi phục hàng đống các từ duy nhất) hoặc giải pháp AWK (chỉ sắp xếp các từ duy nhất) cho tôi. Trừ khi bạn sắp xếp tất cả các từ, như trong tập lệnh shell của McIlroy, thì không nên là O (N log N).
Andriy Makukha

@AndriyMakukha Nó chấm điểm tất cả các tính. Đây là những gì anh chàng biểu diễn của chúng tôi đã viết cho tôi: Độ phức tạp thời gian là O (N log N), trừ khi bạn tin một số điều không rõ ràng về mặt lý thuyết về bảng băm, trong trường hợp đó là O (N).
Adám

Chà, khi tôi chạy mã của bạn chống lại 8, 16 và 32 Ulysses, nó sẽ chậm lại chính xác theo tuyến tính. Có thể anh chàng hiệu suất của bạn cần xem xét lại quan điểm của mình về độ phức tạp thời gian của bảng băm :) Ngoài ra, mã này không hoạt động cho trường hợp thử nghiệm lớn hơn. Nó trả về WS FULL, mặc dù tôi đã tăng không gian làm việc lên 6 GB.
Andriy Makukha

3

Rỉ

Trên máy tính của tôi, điều này chạy giganigs 100000 nhanh hơn khoảng 42% (10,64 giây so với 18,24 giây) so với cây tiền tố C Moo của Moogie + giải pháp thùng C. Ngoài ra, nó không có giới hạn được xác định trước (không giống như giải pháp C xác định trước các giới hạn về độ dài từ, từ duy nhất, từ lặp lại, v.v.).

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

Sử dụng

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

1
Tuyệt vời! Hiệu suất rất tốt trên cả ba cài đặt. Tôi thực sự chỉ đang xem một cuộc nói chuyện gần đây của Carol Nichols về Rust :) Một cú pháp bất thường, nhưng tôi rất hào hứng khi học ngôn ngữ này: dường như là ngôn ngữ duy nhất trong số các ngôn ngữ hệ thống sau C ++ không hy sinh nhiều hiệu suất trong khi làm cho cuộc sống của nhà phát triển dễ dàng hơn nhiều.
Andriy Makukha

Rất nhanh! tôi rất ấn tượng! Tôi tự hỏi nếu tùy chọn trình biên dịch tốt hơn cho C (cây + bin) sẽ cho kết quả tương tự?
Moogie

@Moogie Tôi đã thử nghiệm với bạn -O3-Ofastkhông tạo ra sự khác biệt có thể đo lường được.
Anders Kaseorg

@Moogie, tôi đã biên dịch mã của bạn như thế nào gcc -O3 -march=native -mtune=native program.c.
Andriy Makukha

@Andriy Makukha ah. Điều đó sẽ giải thích sự khác biệt lớn về tốc độ giữa các kết quả bạn nhận được so với kết quả của tôi: bạn đã áp dụng các cờ tối ưu hóa. Tôi không nghĩ có nhiều tối ưu hóa mã lớn còn lại. Tôi không thể kiểm tra bằng cách sử dụng bản đồ theo đề xuất của người khác vì mingw chết không có triển khai ... Và sẽ chỉ tăng 5%. Tôi nghĩ rằng tôi sẽ phải nhường cho mục nhập tuyệt vời của Anders. Làm tốt!
Moogie

2

[C] Cây tiền tố + Thùng

LƯU Ý: Trình biên dịch được sử dụng có ảnh hưởng đáng kể đến tốc độ thực hiện chương trình! Tôi đã sử dụng gcc (MinGW.org GCC-8.2.0-3) 8.2.0. Khi sử dụng côngtắc -Ofast , chương trình chạy nhanh hơn gần 50% so với chương trình được biên dịch thông thường.

Độ phức tạp thuật toán

Kể từ đó tôi nhận ra rằng việc sắp xếp Bin mà tôi đang thực hiện là một dạng sắp xếp Pigeonhost, điều này có nghĩa là tôi có thể làm mất đi sự phức tạp của Big O của giải pháp này.

Tôi tính toán nó là:

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

Độ phức tạp xây dựng cây tương đương với chuyển động của cây vì vậy ở bất kỳ cấp độ nào, nút chính xác để đi qua là O (1) (vì mỗi chữ cái được ánh xạ trực tiếp đến một nút và chúng ta luôn chỉ đi qua một cấp độ của cây cho mỗi chữ cái)

Sắp xếp Pigeon Hole là O (N + n) trong đó n là phạm vi của các giá trị chính, tuy nhiên đối với vấn đề này, chúng ta không cần sắp xếp tất cả các giá trị, chỉ có số k nên trường hợp xấu nhất sẽ là O (N + k).

Kết hợp với nhau thu được O (1 + N + k).

Độ phức tạp không gian để xây dựng cây là do trường hợp xấu nhất là các nút 26 * M nếu dữ liệu bao gồm một từ có số chữ M và mỗi nút có 26 nút (nghĩa là các chữ cái trong bảng chữ cái). Do đó O (26 * M) = O (M)

Đối với phân loại Pigeon Hole có độ phức tạp không gian là O (N + n)

Kết hợp với nhau mang lại O (26 * M + N + n) = O (M + N + n)

Thuật toán

Phải mất hai đối số làm đầu vào (đường dẫn đến tệp văn bản và cho k số từ thường xuyên nhất để liệt kê)

Dựa trên các mục khác của tôi, phiên bản này có một khoảng thời gian rất nhỏ với giá trị k tăng so với các giải pháp khác của tôi. Nhưng đáng chú ý là chậm hơn đối với các giá trị k thấp, tuy nhiên, nó sẽ nhanh hơn nhiều đối với các giá trị k lớn hơn.

Nó tạo ra một nhánh cây trên các chữ cái của từ, sau đó tại các chữ cái lá, nó tăng một bộ đếm. Sau đó thêm từ vào một thùng các từ có cùng kích thước (sau lần đầu tiên xóa từ đó khỏi thùng, nó đã nằm trong đó). Tất cả điều này lặp lại cho đến khi không còn chữ cái nào được đọc nữa. Sau đó, các thùng được lặp lại k lần bắt đầu từ thùng lớn nhất và các từ của mỗi thùng được xuất ra.

Hiện tại nó mặc định để xuất thời gian xử lý, nhưng với mục đích thống nhất với các lần gửi khác, hãy vô hiệu hóa định nghĩa TIMING trong mã nguồ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;
}

EDIT: bây giờ trì hoãn các thùng dân cư cho đến khi cây được xây dựng và tối ưu hóa việc xây dựng đầu ra.

EDIT2: hiện đang sử dụng số học con trỏ thay vì truy cập mảng để tối ưu hóa tốc độ.


Ồ 100.000 từ thường xuyên nhất từ ​​tệp 1 GB trong 11 giây ... Điều này trông giống như một loại trò ảo thuật.
Andriy Makukha

Không có thủ thuật ... Chỉ giao dịch thời gian CPU để sử dụng bộ nhớ hiệu quả. Tôi ngạc nhiên với kết quả của bạn ... Trên máy tính cũ của tôi mất hơn 60 giây. Tôi đã nhận thấy rằng tôi đang thực hiện các so sánh không cần thiết và có thể trì hoãn việc tạo thùng cho đến khi tệp được xử lý. Nó sẽ làm cho nó thậm chí nhanh hơn. Tôi sẽ thử nó sớm và cập nhật câu trả lời của tôi.
Moogie

@AndriyMakukha Bây giờ tôi đã hoãn việc điền vào Thùng cho đến khi tất cả các từ đã được xử lý và cây được xây dựng. Điều này tránh sự so sánh không cần thiết và thao tác phần tử bin. Tôi cũng đã thay đổi cách xây dựng đầu ra vì tôi thấy việc in ấn đang chiếm một lượng thời gian đáng kể!
Moogie

Trên máy của tôi bản cập nhật này không tạo ra bất kỳ sự khác biệt đáng chú ý nào. Tuy nhiên, nó đã thực hiện rất nhanh trên ulysses64một lần, vì vậy nó là một nhà lãnh đạo hiện tại.
Andriy Makukha

Phải là một vấn đề duy nhất với PC của tôi rồi :) Tôi nhận thấy tốc độ tăng thêm 5 giây khi sử dụng thuật toán đầu ra mới này
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

Chạy như một kịch bản với jconsole <script> <input> <k>. Ví dụ: đầu ra từ giganovelvới 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

Không có giới hạn ngoại trừ số lượng bộ nhớ hệ thống có sẵn.


Rất nhanh cho trường hợp thử nghiệm nhỏ hơn! Đẹp! Tuy nhiên, đối với các từ lớn tùy ý, nó cắt các từ trong đầu ra. Tôi không chắc chắn nếu có giới hạn về số lượng ký tự trong một từ hoặc nếu nó chỉ để làm cho đầu ra ngắn gọn hơn.
Andriy Makukha

@AndriyMakukha Vâng, việc ...này xảy ra do cắt đầu ra trên mỗi dòng. Tôi đã thêm một dòng khi bắt đầu để vô hiệu hóa tất cả cắt ngắn. Nó làm chậm gigan xẻ vì nó sử dụng nhiều bộ nhớ hơn vì có nhiều từ độc đáo hơn.
dặm

Tuyệt quá! Bây giờ nó vượt qua bài kiểm tra tổng quát. Và nó đã không làm chậm máy của tôi. Trong thực tế, đã có một sự tăng tốc nhỏ.
Andriy Makukha

1

Con trăn 3

Việc thực hiện với một từ điển đơn giản này nhanh hơn một chút so với sử dụng Countermột từ trên hệ thống của tôi.

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
Tôi chỉ có thể kiểm tra bằng gigan xẻng trên hệ thống của mình và phải mất khá nhiều thời gian (~ 90 giây). gutenbergproject bị chặn ở Đức vì lý do pháp lý ...
Movatica

Hấp dẫn. Nó heapqkhông thêm bất kỳ hiệu suất nào vào Counter.most_commonphương thức, hoặc enumerate(sorted(...))cũng sử dụng heapqnội bộ.
Andriy Makukha

Tôi đã thử nghiệm với Python 2 và hiệu suất tương tự nhau, vì vậy, tôi đoán, việc sắp xếp chỉ hoạt động nhanh như vậy Counter.most_common.
Andriy Makukha

Vâng, có lẽ nó chỉ là jitter trên hệ thống của tôi ... Ít nhất là nó không chậm hơn :) Nhưng tìm kiếm regex nhanh hơn rất nhiều so với việc lặp qua các ký tự. Nó dường như được thực hiện khá hiệu quả.
Movatica

1

[C] Cây tiền tố + Danh sách liên kết được sắp xếp

Phải mất hai đối số làm đầu vào (đường dẫn đến tệp văn bản và cho k số từ thường xuyên nhất để liệt kê)

Dựa trên mục khác của tôi, phiên bản này nhanh hơn nhiều đối với các giá trị k lớn hơn nhưng với chi phí hiệu suất nhỏ với giá trị k thấp hơn.

Nó tạo ra một nhánh cây trên các chữ cái của từ, sau đó tại các chữ cái lá, nó tăng một bộ đếm. Sau đó kiểm tra xem bộ đếm lá hiện tại có lớn hơn từ thường xuyên nhất nhỏ nhất trong danh sách các từ thường xuyên nhất không. (kích thước danh sách là số được xác định thông qua đối số dòng lệnh) Nếu vậy thì hãy quảng bá từ được biểu thị bằng chữ cái lá là một trong những từ thường xuyên nhất. Nếu đã là một từ thường xuyên nhất, sau đó trao đổi với từ thường xuyên nhất tiếp theo nếu số từ hiện tại cao hơn, do đó giữ cho danh sách được sắp xếp. Tất cả điều này lặp lại cho đến khi không còn chữ cái nào được đọc. Sau đó, danh sách các từ thường xuyên nhất được xuất ra.

Hiện tại nó mặc định để xuất thời gian xử lý, nhưng với mục đích thống nhất với các lần gửi khác, hãy vô hiệu hóa định nghĩa TIMING trong mã nguồ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

#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;
}

Nó trả về đầu ra không được sắp xếp cho k = 100.000 : 12 eroilk 111 iennoa 10 yttelen 110 engyt.
Andriy Makukha

Tôi nghĩ rằng tôi có một ý tưởng như lý do. Suy nghĩ của tôi là tôi sẽ cần lặp lại các từ hoán đổi trong danh sách khi kiểm tra xem từ cao nhất tiếp theo của từ hiện tại. Khi nào có thời gian tôi sẽ kiểm tra
Moogie

hmm, có vẻ như cách khắc phục đơn giản là thay đổi if nếu vẫn hoạt động, tuy nhiên nó cũng làm chậm đáng kể thuật toán cho các giá trị lớn hơn của k. Tôi có thể phải nghĩ ra một giải pháp thông minh hơn.
Moogie

1

C #

Cái này sẽ hoạt động với SDK .net mới nhất .

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);
    }
}

Đây là một đầu ra mẫu.

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

Lúc đầu, tôi đã thử sử dụng một từ điển với các khóa chuỗi, nhưng cách đó quá chậm. Tôi nghĩ đó là bởi vì các chuỗi .net được thể hiện bên trong bằng mã hóa 2 byte, điều này gây lãng phí cho ứng dụng này. Vì vậy, sau đó tôi chỉ chuyển sang các byte thuần và một máy trạng thái kiểu goto xấu xí. Chuyển đổi trường hợp là một toán tử bitwise. Kiểm tra phạm vi ký tự được thực hiện trong một so sánh duy nhất sau khi trừ. Tôi đã không dành bất kỳ nỗ lực nào để tối ưu hóa loại cuối cùng vì tôi thấy nó sử dụng ít hơn 0,1% thời gian chạy.

Khắc phục: Thuật toán về cơ bản là chính xác, nhưng nó đã báo cáo quá mức tổng số từ, bằng cách đếm tất cả các tiền tố của từ. Vì tổng số từ không phải là một yêu cầu của vấn đề, tôi đã loại bỏ đầu ra đó. Để xuất ra tất cả các từ k, tôi cũng điều chỉnh đầu ra. Cuối cùng tôi đã quyết định sử dụng string.Join()và sau đó viết toàn bộ danh sách cùng một lúc. Đáng ngạc nhiên là điều này nhanh hơn khoảng một giây trên máy của tôi khi viết riêng từng từ với giá 100k.


1
Rất ấn tượng! Tôi thích tolowerthủ thuật so sánh bitwise và duy nhất của bạn . Tuy nhiên, tôi không hiểu tại sao chương trình của bạn báo cáo các từ khác biệt hơn mong đợi. Ngoài ra, theo mô tả vấn đề ban đầu, chương trình cần xuất ra tất cả các từ k theo thứ tự tần số giảm dần, vì vậy tôi đã không tính chương trình của bạn vào bài kiểm tra cuối cùng, cần xuất ra 100.000 từ thường xuyên nhất.
Andriy Makukha

@AndriyMakukha: Tôi có thể thấy rằng tôi cũng đang đếm các tiền tố từ không bao giờ xảy ra trong số cuối cùng. Tôi tránh viết tất cả đầu ra vì đầu ra giao diện điều khiển khá chậm trong windows. Tôi có thể ghi đầu ra vào một tập tin không?
đệ quy

Chỉ cần in nó đầu ra tiêu chuẩn, xin vui lòng. Với k = 10, nó phải nhanh trên bất kỳ máy nào. Bạn cũng có thể chuyển hướng đầu ra thành một tệp từ một dòng lệnh. Như thế này .
Andriy Makukha

@AndriyMakukha: Tôi tin rằng tôi đã giải quyết tất cả các vấn đề. Tôi tìm thấy một cách để sản xuất tất cả các đầu ra cần thiết mà không tốn nhiều thời gian chạy.
đệ quy

Đầu ra này là nhanh chóng! Rất đẹp. Tôi đã sửa đổi chương trình của bạn để in số lượng tần số, như các giải pháp khác làm.
Andriy Makukha

1

Ruby 2.7.0-preview1 với tally

Phiên bản mới nhất của Ruby có một phương thức mới gọi là tally. Từ ghi chú phát hành :

Enumerable#tallyđược thêm vào. Nó đếm sự xuất hiện của từng yếu tố.

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

Điều này gần như giải quyết toàn bộ nhiệm vụ cho chúng tôi. Chúng ta chỉ cần đọc tệp trước và tìm max sau.

Đây là toàn bộ:

k = ARGV.shift.to_i

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

chỉnh sửa: Đã thêm kdưới dạng đối số dòng lệnh

Nó có thể được chạy bằng ruby k filename.rb input.txtcách sử dụng phiên bản 2.7.0-preview1 của Ruby. Điều này có thể được tải xuống từ các liên kết khác nhau trên trang ghi chú phát hành hoặc được cài đặt với rbenv bằng cách sử dụng rbenv install 2.7.0-dev.

Ví dụ chạy trên máy tính cũ của tôi.

$ 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
Tôi đã cài đặt Ruby từ các nguồn. Nó chạy nhanh như trên máy của bạn (15 giây so với 17).
Andriy Makukha
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.