65.9K
CodeProject 正在变化。 阅读更多。
Home

计数出现次数

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (7投票s)

2014年2月6日

CPOL

5分钟阅读

viewsIcon

33838

downloadIcon

95

一种用于计算文件中符号出现次数的简单算法

引言

计数符号的出现次数可以回答这样的问题:“这段文本中有多少个空格?”。除此之外,它还可以用作某些数据压缩算法的预处理步骤。

在这里,我将展示使用 C 和 Lua 编程语言实现的简单算法。此外,还提供了 C++ 实现供下载。为简洁起见,此处不一一列出。

问题

给定一些输入数据,例如“hello world!”,生成类似以下格式的输出

符号code出现次数
o1112
w1191
r1141
h1041
e1011
d1001
321

其中“code”是字符的 ASCII 码(字节的值)。由于我们要处理大型二进制文件,让我们这样表述:“计算二进制文件中每个字节值(0..255)的出现次数,然后按出现次数排序”。

算法

该算法非常简单:假设我们一次读取一个字节的数据,当前字节的值是,例如,104(即刚刚读取了字符 'h'):我们需要增加 104 的出现次数,并对下一个字节重复此操作,直到所有输入数据都处理完毕。一种快速“增加字节值出现次数”的方法是使用一个出现次数数组,其隐含的假设是某个值的出现次数存储在该值本身的索引所对应的项中(例如,104 的出现次数存储在 occurrences[104] 中)。最后对该数组进行排序以获得预期的输出。

C 代码

C 实现相当直接:程序初始化出现次数数组,然后使用 4KiB 的缓冲区从 stdin 读取数据。每次读取操作后,都会调用一个函数来更新出现次数数组。最后,使用 C 库的 qsort 函数执行排序步骤。

以下是存储符号代码及其出现次数的 struct(我们需要存储符号代码以便执行排序步骤)。

#define BYTES 256
 
typedef struct TagSymbolInfo
{
  int symbol; // the byte [0..255] value
  int occurrences; // number of occurrences of the symbol in the given buffer
} SymbolInfo;

初始化函数(设置符号代码,重置出现次数)。

// initialise the symbol array
void reset_occurrences(SymbolInfo si[])
{
  int n;
  for (n = 0; n < BYTES; ++n)
  {
    si[n].symbol = n;
    si[n].occurrences = 0;
  }
}

以下函数是该算法的核心:它处理输入数据以更新出现次数数组。

 // counts the occurrences in the given buffer, updating accordingly the array of symbols
int increment_occurrences(SymbolInfo si[], const void * buffer, size_t size)
{
  unsigned char * p = (unsigned char *) buffer;
  int success = 0;
 
  while ( size-- )
  {
    si[p[size]].occurrences++; // since si[n].symbol == n, we can use p[size] as index.
    if ( ! si[p[size]].occurrences )
      success = -1; // error on counter overflow
  }
  return success;
} 

用于按出现次数(出现次数相同时按代码)对项进行排序的函数。

// this is the predicate for qsort.
int compare_symbols(const void * symbola, const void * symbolb)
{
  const SymbolInfo * a = (const SymbolInfo *) symbola;
  const SymbolInfo * b = (const SymbolInfo *) symbolb;
 
  if ( a->occurrences > b->occurrences || 
  (a->occurrences == b->occurrences && a->symbol > b->symbol)) return -1;
  return 1;
}

最后是主函数,附带一些样板代码。

 #define INBUFSIZE 4*1024
 
int main()
{
  long total = 0; // total number of occurrences, must be equal to file size
  unsigned char buffer[INBUFSIZE]; // the read buffer
  int n;
  SymbolInfo si[BYTES]; // the array of symbols

  reset_occurrences(si); // initialization

  // reading stdin until EOF (or error)
  for(;;)
  {
    int nread = fread (buffer, sizeof(buffer[0]), INBUFSIZE, stdin);
    if ( ! nread ) break;
    if ( increment_occurrences(si, buffer, nread) )
      printf("occurrences counter(s) overflow\n");
  }
  // calling qsort for the sorting step
  qsort( si, BYTES, sizeof(si[0]), compare_symbols);
 
 
  // some output
  for (n=0; n<BYTES; ++n)
  {
    char c = si[n].symbol;
    if (! si[n].occurrences) break;
    total += si[n].occurrences;
 
    c = isprint(c) ?  c : '.';
 
    printf("{symbol ='%c', code= %3d,  
    occurrences = %d}\n", c, si[n].symbol, si[n].occurrences);
  }
  printf("total occurrences = %ld\n", total);
 
  return 0;
}

Lua 代码

Lua 是一种很棒的脚本语言,轻量级、快速且易于嵌入(更多信息可在 Lua 网站找到)。

由于它在 CodeProject 上是一种“比较冷门”的语言,因此我将对提供的代码做一些详细说明。

local INBUFSIZE = 4096
 
local function init_occurrences()
  local si = {}
  for i=1,256 do
    si[i] = { occurrences = 0, symbol = (i-1) }
  end
  return si
end

init_occurrences 函数创建 *出现次数数组* si 并对其进行初始化。

更详细地说,local si = {} 语句创建了一个 table,这是 Lua 提供的 *唯一* 数据结构。

表是一种关联数组。在 for 循环内部,会创建一个新的表,初始化并赋值给 si 的每个项。

语句...

si[i] = { occurrences = 0, symbol = (i-1) } 

...是以下语句的快捷方式:

s[i] = {}
s[i]["occurrences"] = 0
s[i]["symbol"] = i-1 

或者...

s[i] = {}
s[i].occurrences = 0
s[i].symbol = i-1 

...使用点运算符提供的“语法糖”。

最后,return si 终止函数并将 si 提供给调用者,避免其在下次垃圾回收时被移除。

increment_occurrences 函数相当直接,它展示了缺少方便的增量运算符。

 local function increment_occurrences(si, buffer)
  for i=1,#buffer do
    local index = buffer:byte(i)+1
    si[index].occurrences = si[index].occurrences + 1
  end
end 

程序的其余部分,样板代码...

 local function main()
 
  local total = 0
  local si = init_occurrences()
 
  repeat
    local buf = io.read(INBUFSIZE)
    if  buf then
      increment_occurrences(si, buf)
    end
  until not buf
 
  table.sort(si,
    function (a,b)
      if (a.occurrences > b.occurrences or (a.occurrences == b.occurrences and a.symbol > b.symbol)) then
        return true
      end
    end)
 
  for i=1,256 do
    if si[i].occurrences == 0 then break end
    local c = string.char(i):match("([%g])") or "-"
 
    print(string.format("{symbol ='%s', code= %3d,  occurrences = %d}", c, si[i].symbol, si[i].occurrences));
 
    total = total + si[i].occurrences
  end
 
  print("total occurrences = " .. total)
end 

main() 

有一个不错的亮点:接下来的语句使用 table.sort 函数和一个 *匿名* 函数对 si 表进行排序。

  table.sort(si,
    function (a,b)
      if (a.occurrences > b.occurrences or (a.occurrences == b.occurrences and a.symbol > b.symbol)) then
        return true
      end
    end)

C++ 代码

David Delaune,又名 Randor 向我建议了一种巧妙的输入读取方法(您可以在页面底部的讨论串中找到他的评论)。现在展示 C++ 代码是很有意义的。

struct SymbolInfo
{
  int symbol; // the byte value [0..255]
  int occurrences; // the number of occurrences of the symbol
};

int main()
{
  std::cin >> std::noskipws;
  std::array<SymbolInfo , 256> si={{0}};
  std::istream_iterator<unsigned char> input(std::cin),end;
  std::for_each(input,end,[&](unsigned char c){si[c].occurrences++;});

  for (int n=0; n<256; ++n) si[n].symbol=n;

  std::sort(si.begin(), si.end(),
    [](const SymbolInfo & a, const SymbolInfo &b) {return (a.occurrences > b.occurrences || (a.occurrences == b.occurrences && a.symbol > b.symbol));}
  );
}

就是这样!(我只是省略了 include 指令和样板打印代码)。

Using the Code

目前,C/C++ 代码可以在 Linux 控制台上运行(尽管将其修改为在 Windows 控制台上运行非常简单)。

Lua 代码需要 Lua 解释器,在类 Ubuntu 发行版中作为 lua5.2 包(apt-get install lua5.2)提供。此外,在此处可以找到 Lua 的二进制文件(也包括 Windows 版本):Lua 二进制文件在 SourceForge

C 代码

使用以下命令编译:

 gcc -Wall -O3 -o occurrences occurrences.c 

使用以下命令运行:

./occurrences < <FILE PATH>

(例如 ./occurrences < myfile.bin

为了计算文件的出现次数,必须使用输入重定向运算符(<)(否则命令将计算 stdin 中的出现次数,即用户输入的字符直到 CTRL^D)。

C++ 代码

使用以下命令编译程序 (1):

 g++ -std=c++11 -Wall -O3 -o occurrences occurrences.cpp
并使用以下命令编译程序 (2):

g++ -std=c++11 -Wall -O3 -o occurrences_ran occurrences_ran.cpp  

(C++ 程序需要较新版本的 g++,我使用的是 4.7.3 版本)

像 C 程序一样运行。

Lua 代码

当然,它不需要编译。您可以这样运行它:

lua occurrences.lua < <FILE PATH>  

基准测试

我知道使用一个相当底层的算法来比较 Lua 和 C/C++ 代码的性能是不公平的,但我并不公平,所以这里有一个小测试。

详细说明
  • Lubuntu 13.04 系统上执行。
  • 输入文件:lubuntu-13.10-desktop-i386.iso (729,808,896 字节)。
  • g++gcc 的优化级别为 -O3
  • Lua 版本为 5.2.1
  • 报告的时间是通过 Linux time 命令生成的。
language文件大小 (MiB)实际时间 (秒)用户时间 (秒)系统时间 (秒)速度 (MiB/秒)
C++ (1)6969.071.311.5176.74
C++ (2)69641.4737.203.5016.78
C6969.940.621.6370.02
Lua696165.38155.874.384.21
注释
  1. '传统' C++ 程序(源代码文件 occurrences.cpp 可在下载部分找到)
  2. 更优雅的程序(文章中展示的那个,可在下载部分找到,名为 occurrences_ran.cpp
备注
  • 显然 C 和 C++ (1) 的性能非常相似。正如 realusersys 图所示,它们主要受 I/O 限制。
  • C++ (2) 比 C++ (1) 明显慢。
  • LuaC/C++ 慢大约 17 倍,其执行时间主要由 user 部分决定。

历史

  • 2014 年 2 月 8 日 - 添加了 C++ 代码(感谢 David Delaune aka Randor
  • 2014 年 2 月 6 日 - 添加了下载所需的源文件
  • 2014 年 2 月 6 日 - 首次发布
© . All rights reserved.