二、程序功能描述
程序实现的功能:对文本文件进行压缩以及对压缩的文本文件进行解压缩。程序的实现的理论依据是赫夫曼编码。赫夫曼编码是一种无损的压缩算法,一般用来压缩文本和程序文件。赫夫曼压缩属于可变代码长度算法一族。意思是个体符号(例如,文本文件中的字符)用一个特定长度的位序列替代。因此,在文件中出现频率高的符号,使用短的位序列,而那些很少出现的符号,则用较长的位序列。
程序由三个文件组成:头文件CourseDesign.h、函数实现文件CourseDesign.cpp、测试文件Test.cpp。在CourseDesign.h中声明数据的存储结构以及程序所需要的处理函数;CourseDesign.cpp文件实现在CourseDesign.h中声明的函数;Test.cpp负责对所实现的函数进行调用测试,确定是否满足程序设计要求。
利用赫夫曼编码实现对文本的压缩的过程大致为:打开要压缩的文本文件,读取文件中的字符,统计文件中不同字符出现的频率,建立赫夫曼树,通过赫夫曼树对出现的互不相同的字符进行编码,建立编码表,接着将将赫夫曼树(即解码表)写入压缩文件中。再次重新读取文件中的字符,对每个字符通过查阅编码表确定对应的编码,将该字符的赫夫曼编码写入压缩文件。对压缩文件的解压过程为:打开压缩文件,读取压缩文件解码表部分,读取压缩文件的编码数据,将压缩数据通过解码表进行解码,将解码出的字符写入解码文件中。
程序执行后,用户按照程序的提示选择相应的功能选项。当用户选择压缩功能,此时程序要求用户输入要压缩的文本文件的路径,用户输入完成后。程序检查文件是否能够建立。检查完成后,程序将文件从硬盘读入内存。接着程序将统计不同字符出现的频率以及建立编码表的初步结构。例如当文件中存有如下表所示字符。
表1 文件字符属性表
字符 | 第一字节机内码/ASCII | 第二字节机内码 | 权重 |
的 | 181 | 196 | 20 |
a | 97 | 0 | 9 |
把 | 176 | 209 | 14 |
表 | 177 | 237 | 5 |
班 | 176 | 224 | 1 |
补 | 178 | 185 | 2 |
百 | 176 | 214 | 17 |
防 | 183 | 192 | 12 |
飞 | 183 | 201 | 9 |
博 | 178 | 169 | 13 |
包 | 176 | 252 | 2 |
才 | 178 | 197 | 6 |
方 | 183 | 189 | 8 |
拜 | 176 | 221 | 3 |
A | 65 | 0 | 3 |
份 | 183 | 221 | 5 |
必 | 177 | 216 | 5 |
英文字符在计算机中是以标准ASCII码进行表示,一个英文字符占一个字节空间,编码范围为0~127;汉字在计算机中是以GB2312编码进行表示。每个汉字占两个字节存储空间,汉字的存储使用机内码,各字节的机内码编码范围为160~254。现在需要考虑使用怎样的数据结构来存放这些字符,如果采用如下简单的数据结构存放:
typedef struct
{
int
int
int
char
}CodeList, *CodePList;
分析所要处理的字符数据会发现:许多的字符的第一字节的机内码相同,如“防”、“飞”、“方”、“份”,第一字节机内码都是183。这是因为汉字是通过划分区号和位号来表示的,所有汉字被划分成了94个区,94个位,所以当汉字属于同一个区,那么它的第一字节机内码就会相同。如果采用如上的数据结构建立的线性表来存放处理字符,就会存在大量数据冗余。
在这种情况下,就有必要为特定的数据设计合适的数据结构。通过分析,采用如下数据结构:
typedef struct
{
}InternalCode;
typedef struct
{
该结构的优点:当汉字的第一字节机内码相同,则该第一字节机内码只会被存储一次,从而消除汉字第一字节机内码存储的冗余,而且可以方便的使用折半查找快速检索编码表来确定字符的赫夫曼编码。采用该数据结构对表1数据进行表示如图1。
图1 编码表HC的存储结构
这种数据结构形式上类似于图的邻接表结构,功能上类似于哈希表的链地址法。但邻接表的表结点采用链式存储,而图1的表结点和头结点都采用线性表储存。图1中编码表HC的内码1是纵向非递减排列,内码2是横向非递减排列。HC[i].count – HC[i – 1].count等于HC[i]实际存储的字符数量。例如, HC[3]中字符数为7,HC[2]中字符数为2,则HC[3]存放了5个字符,这5个字符拥有相同的第一字节机内码176。
程序执行压缩操作详细过程:当程序从文件中读取一个字符后,通过字符的编码范围分析该字符是属于ASCII还是GB2312,若是ASCII编码,增加编码表HC纵向表长,将该字符的ASCII码按非递减次序插入到内码1处,并将当前位置的字符数加1,并置内码2默认为0;如果是汉字,首先通过折半查找算法纵向查找编码表HC的内码1成员,若当前汉字第一字节机内码已经出现过,则折半查找返回该机内码1在HC表中的位置,增加当前位置的横向表长,将汉字的第二字节机内码按非递减次序插入当前位置的内码2处。否则将汉字的第一字节机内码按非递减次序插入HC表的内码1区域,第二字节机内码直接插入内码2处。在读取文件的同时记录文件中各字符出现的频率,当编码表HC表构建完成,此时w = {3, 9, 14, 3, 1, 2, 17, 5, 5, 13, 2, 6, 20, 9, 8, 5, 12}。依次从w中选择权重最小并且双亲为0的两个结点,根据这两个结点生成新的结点,新结点的权重为这两个最小结点的和,新结点的左右子树为这两个结点在w中的位置。根据表1数据构建赫夫曼树如图2所示。赫夫曼树存储结构的初始状态如图3(a),终结状态如图3(b)。
图2 根据表1构造的赫夫曼树
图3 (a)
根据生成的赫夫曼树对HC表中的字符进行编码,编码的方法:从该叶子到根逆向求该字符的编码。例如图2中“把”的权值为14,对应的编码为:“000”。将得到的赫夫曼编码写入HC[i].internal_code_address[j].code指向的区域。当字符编码完成之后,打开压缩文件,将赫夫曼树HT中除权重以外的数据(解码无需权重信息)写入压缩文件中,作为下一次解压缩的解码表。再次打开要压缩的文本文件,读取文件中的字符,根据编码的范围确定该字符是ASCII还是GB2312,如果ASCII则调用折半查找函数,在编码表HC中进行纵向查找,查找此ASCII出现的位置p1,该字符的编码为HC[p1].internal_code_address[1].code;如果字符是汉字,则调用折半查找先纵向查找该汉字的第一字节机内码在HC中的位置p1,然后从HC[p1].internal_code_address开始横向查找该汉字的第二字节机内码的位置p2,这样就得到了该汉字的赫夫曼编码为HC[p1].internal_code_address[p2].code因为赫夫曼编码在HC表中是以字符串形式存放(因为计算机的基本储单位是字节,如果以位存放,需要另设一个空间来表示未编码的位空间大小)。所以需要将字符串“0101“转换为二进制0101写入文件。因为每个赫夫曼编码的长度是不一样的,假设某字符的赫夫曼长度为4,则将该编码写入一个字节后,还剩余4个位,则下一次可以继续从第5个位开始写入,当所有字符的编码都写入完毕后,最后一个字节并不一定会用完,所以需要附设一个字节来记录最后一个字符编码实际写入的编码位数。编码文件的结构如下图所示:
程序解压文件:打开压缩文件,取出压缩文件的解码表长度N,根据N读取N条解码表记录,重建解码表HT,然后读取压缩数据DATA,解码的过程是从根出发,按DATA数据的0或1确定找左子树还是右子树,直至叶子结点,便求得了DATA相应的字符。将字符写入文件,直至所有DATA数据处理完毕,整个解压过程结束。
三、程序源代码
1. 头文件CourseDesign.h
- #ifndef _COURSEDESIGN_H_
- #define _COURSEDESIGN_H_
- //-----Huffman树存储结构
- typedef struct
- {
- char ch[3];
- unsigned int weight;
- unsigned int parent, lchild, rchild;
- }HTNode, *HuffmanTree;
- //----Huffman编码表存储结构
- typedef struct
- {
- char internal_code;
- char *code;
- }InternalCode;
- typedef struct
- {
- int count;
- char internal_code;
- InternalCode *internal_code_address;
- }HuffmanCode, *HuffmanPCode;
- //-------解码表存储结构
- typedef struct
- {
- char ch[3];
- unsigned int lchild, rchild;
- }DecodeList, *DecodePList;
- //------辅助数组,置/取一个字节的指定位
- const static unsigned char mask[8] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
- template<class T> static int xj_Search_Bin(int key, T L, int low, int high);
- template<class T> static void xj_InsertSort(T L , int start, int end);
- void xj_Select(const HuffmanTree HT, int n, int &s1, int &s2);
- void xj_Statistics(HuffmanPCode &HC, int internal_code1, int internal_code2, int (*FrequencyMeter)[255], int &n);
- bool xj_Init(char *filename, HuffmanPCode &HC, int *&w, int &n);
- void xj_CreateHuffmanTree(HuffmanTree &HT, const HuffmanPCode HC, const int *w, int n);
- void xj_HuffmanCoding(const HuffmanTree HT, HuffmanPCode HC, int n);
- bool xj_Compress(char *ifilename, char *ofilename, const HuffmanPCode HC, const HuffmanTree HT, int n);
- bool xj_DeCompress(char *ifilename, char *ofilename);
- void xj_Interface();
- #endif
2. 函数实现文件CourseDesign.cpp
- #include"CourseDesign.h"
- #include<iostream>
- #include<fstream>
- #include<iomanip>
- #include<malloc.h>
- #include<string.h>
- using namespace std;
- //-----------折半查找----------------
- template<class T>
- int xj_Search_Bin(int key, T L, int low, int high)
- {
- int mid = 0;
- int internal_code;
- while (low <= high)
- {
- mid = (low + high) / 2;
- internal_code = int(L[mid].internal_code & 0xFF);
- if (key == internal_code)
- {
- return mid;
- }
- else if (internal_code > key)
- {
- high = mid - 1;
- }
- else
- {
- low = mid + 1;
- }
- }
- return 0;
- }
- //--------对HC表的字符域做插入非递减排序-----
- template<class T>
- void xj_InsertSort(T L , int start, int end)
- {
- int i;
- L[0] = L[end];
- i = end - 1;
- while (i >= start && int(L[i].internal_code & 0xFF) > int(L[0].internal_code & 0xFF))
- {
- L[i + 1] = L[i];
- i--;
- }
- L[i + 1] = L[0];
- }
- //------- 寻找权重最小的两个结点----------------------
- void xj_Select(const HuffmanTree HT, int n, int &s1, int &s2)
- {
- int i = 0;
- s1 = s2 = 0;
- for (i = 1; i <= n; ++i)
- {
- if (HT[i].parent == 0)
- {
- if (s1 == 0)
- {
- s1 = i;
- }
- else if (s2 == 0)
- {
- s2 = i;
- }
- else if (HT[i].weight < HT[s1].weight || HT[i].weight < HT[s2].weight)
- {
- s1 = HT[s1].weight < HT[s2].weight ? s1 : s2;
- s2 = i;
- }
- }
- }
- }
- //----构建HC.internal_code以及HC.internal_code_address结构-------------
- void xj_Statistics(HuffmanPCode &HC, int internal_code1, int internal_code2, int (*FrequencyMeter)[255], int &n)
- {
- int position;
- if (internal_code1 < 128)
- {
- if (FrequencyMeter[internal_code1][0] == 0)
- {
- ++n;
- HC = (HuffmanPCode)realloc(HC, (n + 1) * sizeof(HuffmanCode));
- HC[n].internal_code = internal_code1;
- HC[n].count = 1;
- HC[n].internal_code_address = (InternalCode *)malloc(2 * sizeof(InternalCode));
- HC[n].internal_code_address[1].internal_code = 0; //0号单元未用
- xj_InsertSort(HC, 1, n);
- }
- ++FrequencyMeter[internal_code1][0];
- }
- else
- {
- if (FrequencyMeter[internal_code1][internal_code2] == 0)
- {
- position = xj_Search_Bin(internal_code1, HC, 1, n);
- if (position != 0)
- {
- ++HC[position].count;
- HC[position].internal_code_address = (InternalCode *)realloc(HC[position].internal_code_address, (HC[position].count + 1) * sizeof(InternalCode));
- HC[position].internal_code_address[HC[position].count].internal_code = internal_code2;
- xj_InsertSort(HC[position].internal_code_address, 1, HC[position].count);
- }
- else
- {
- ++n;
- HC = (HuffmanPCode)realloc(HC, (n + 1) * sizeof(HuffmanCode));
- HC[n].internal_code = internal_code1;
- HC[n].count = 1;
- HC[n].internal_code_address = (InternalCode *)malloc(2 * sizeof(InternalCode));
- HC[n].internal_code_address[1].internal_code = internal_code2;
- xj_InsertSort(HC, 1, n);
- }
- }
- ++FrequencyMeter[internal_code1][internal_code2];
- }
- }
- //--------统计不同字符出现的频率以及构建HC的机内码成员结构-------
- bool xj_Init(char *filename, HuffmanPCode &HC, int *&w, int &n)
- {
- ifstream ifs(filename);
- int i = 0, j = 0;
- int FrequencyMeter[255][255] = {0};
- char ch1, ch2;
- n = 0;
- HC = NULL;
- w = NULL;
- if (ifs.fail())
- {
- cout<<"can't open file!"<<endl;
- return false;
- }
- while ((ch1 = ifs.get()) != EOF)
- {
- if (int(ch1 & 0xFF) > 128)
- {
- ch2 = ifs.get();
- }
- else
- {
- ch2 = 0;
- }
- xj_Statistics(HC, int(ch1 & 0xFF), int(ch2 & 0xFF), FrequencyMeter, n);
- }
- HC[0].count = 0;
- for (i = 2; i <= n; ++i) HC[i].count += HC[i - 1].count;
- w = (int *)malloc(HC[n].count * sizeof(int));
- for (i = 1; i <= n; ++i)
- {
- for (j = HC[i - 1].count; j < HC[i].count; ++j)
- {
- w[j] = FrequencyMeter[int(HC[i].internal_code & 0xFF)][int(HC[i].internal_code_address[j - HC[i - 1].count + 1].internal_code & 0xFF)];
- }
- }
- ifs.close();
- return true;
- }
- //--------构造赫夫曼树HT---------------------
- void xj_CreateHuffmanTree(HuffmanTree &HT, const HuffmanPCode HC, const int *w, int n)
- {
- int i = 0, j = 0;
- int m = 0, s1 = 0, s2 = 0;
- if (HC[n].count <= 1) return;
- m = 2 * HC[n].count - 1;
- HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode));
- for (i = 1; i <= n; ++i)
- {
- for (j = HC[i - 1].count + 1; j <= HC[i].count; ++j, ++w)
- {
- HT[j].ch[0] = HC[i].internal_code;
- HT[j].ch[1] = HC[i].internal_code_address[j - HC[i - 1].count].internal_code;
- HT[j].ch[2] = '\0';
- HT[j].weight = *w;
- HT[j].lchild = HT[j].rchild = HT[j].parent = 0;
- }
- }
- for (i = HC[n].count + 1; i <= m; ++i)
- {
- *HT[i].ch = 0;
- HT[i].weight = HT[i].lchild = HT[i].rchild = HT[i].parent = 0;
- }
- for (i = HC[n].count + 1; i <= m; ++i)
- {
- xj_Select(HT, i - 1, s1, s2);
- HT[s1].parent = i; HT[s2].parent = i;
- HT[i].lchild = s1; HT[i].rchild = s2;
- HT[i].weight = HT[s1].weight + HT[s2].weight;
- }
- }
- //----------建立编码表HC-------------------
- void xj_HuffmanCoding(const HuffmanTree HT, HuffmanPCode HC, int n)
- {
- int start = 0, c = 0, f = 0;
- int i = 0, k = 1, r = 1; ;
- char *cd = NULL;
- cd = (char *)malloc(HC[n].count * sizeof(char));
- cd[HC[n].count - 1] = '\0';
- for (i = 1; i <= HC[n].count; ++i)
- {
- start = HC[n].count -1;
- for (c = i, f = HT[i].parent; f!= 0; c=f, f = HT[f].parent)
- {
- if (HT[f].lchild == c)
- {
- cd[--start] = '0';
- }
- else
- {
- cd[--start] = '1';
- }
- }
- if (k > HC[r].count - HC[r - 1].count)
- {
- k = 1;
- ++r;
- }
- HC[r].internal_code_address[k].code = (char *)malloc((HC[n].count - start) * sizeof(char));
- strcpy(HC[r].internal_code_address[k].code, &cd[start]);
- ++k;
- }
- free(cd);
- }
- //-------------------------压缩文件--------------
- bool xj_Compress(char *ifilename, char *ofilename, const HuffmanPCode HC, const HuffmanTree HT, int n)
- {
- ifstream ifs(ifilename);
- ofstream ofs(ofilename, ios::binary);
- int bit_size = 0;
- int position1, position2;
- int internal_code1, internal_code2;
- char ch;
- char code = 0;
- char *code_address;
- DecodePList decode_list = (DecodePList)malloc((HC[n].count * 2) * sizeof(DecodeList));
- if (ifs.fail() || ofs.fail())
- {
- cout<<"Can't open the file!"<<endl;
- return false;
- }
- ofs.write((char *)&HC[n].count, sizeof(int)); //写入解码表
- for (int i = 1; i <= 2 * HC[n].count - 1; ++i)
- {
- strcpy(decode_list[i].ch, HT[i].ch);
- decode_list[i].lchild = HT[i].lchild;
- decode_list[i].rchild = HT[i].rchild;
- ofs.write((char *)&decode_list[i], sizeof(DecodeList));
- }
- while ((ch = ifs.get()) != EOF)
- {
- internal_code1 = int(ch & 0xFF);
- position1 = xj_Search_Bin(internal_code1, HC, 1, n);
- if (internal_code1 < 128)
- {
- internal_code2 = 0;
- position2 = 1;
- }
- else
- {
- internal_code2 = int(ifs.get() & 0xFF);
- position2 = xj_Search_Bin(internal_code2, HC[position1].internal_code_address, 1, HC[position1].count - HC[position1 - 1].count);
- }
- code_address = HC[position1].internal_code_address[position2].code;
- while (*code_address)
- {
- code |= (*code_address++ - 48) * mask[bit_size % 8];
- ++bit_size;
- if (bit_size % 8 == 0)
- {
- ofs<<code;
- code = 0;
- }
- }
- }
- if (bit_size % 8 != 0)
- {
- ofs<<code;
- ofs<<char(bit_size % 8);
- }
- else
- {
- ofs<<char(8);
- }
- ifs.clear();
- ifs.seekg(0, ios::end);
- cout<<"压缩完成!"<<endl;
- cout<<"原始文件大小: "<<ifs.tellg()<<" B"<<endl;
- cout<<"压缩文件大小:" <<ofs.tellp()<<" B"<<endl;
- cout<<"压缩率: "<<float(ofs.tellp())/float(ifs.tellg())*100<<" %\n";
- free(decode_list);
- free(HT);
- free(HC);
- ifs.close();
- ofs.close();
- return true;
- }
- //---------------解压缩文件---------------------
- bool xj_DeCompress(char *ifilename, char *ofilename)
- {
- ifstream ifs(ifilename,ios::binary);
- ofstream ofs(ofilename);
- int bit_size;
- int i;
- int m, n;
- char buf;
- int value;
- DecodePList decode_list;
- if (ifs.fail() || ofs.fail())
- {
- cout<<"Can't open the file!"<<endl;
- return false;
- }
- ifs.read((char *)&n, sizeof(int)); // 读取解码表
- m = 2 * n - 1;
- decode_list = (DecodePList)malloc((m + 1) * sizeof(DecodeList));
- for (i = 1; i <= m; ++i)
- {
- ifs.read((char *)&decode_list[i], sizeof(DecodeList));
- }
- streampos pos1 = ifs.tellg();
- ifs.seekg(-1, ios::end);
- streampos pos2 = ifs.tellg();
- bit_size = (int(pos2 - pos1) - 1) * 8 + ifs.get();
- ifs.seekg(pos1, ios::beg);
- for (i = 0; i < bit_size; ++i)
- {
- if (i % 8 == 0)
- {
- ifs.read(&buf, 1);
- value = int(buf & 0xFF);
- }
- if (int(value & mask[i % 8]) != mask[i % 8]) //value编码的i % 8 + 1位是0
- {
- m = decode_list[m].lchild;
- }
- else
- {
- m = decode_list[m].rchild;
- }
- if (decode_list[m].lchild == 0 )
- {
- ofs<<decode_list[m].ch;
- m = 2 *n - 1;
- }
- }
- ifs.close();
- ofs.close();
- free(decode_list);
- cout<<"解压完成!"<<endl;
- return true;
- }
- void xj_Interface()
- {
- cout<<"----------------------------------------------------------------\n\n";
- cout<<" 《基于Huffman编码的文档压缩》\n\n";
- cout<<" 学号:2011150058 姓名:张剑锋 班级: 软工01\n\n";
- cout<<" 请选择功能选项:\n\n";
- cout<<" 1. 压缩文件\n";
- cout<<" 2. 解压缩文件\n";
- cout<<" 3. 退出\n\n";
- cout<<"----------------------------------------------------------------\n";
- }<span style="white-space:pre"> </span>
3. 测试文件Test.cpp
- #include"CourseDesign.h"
- #include <time.h>
- #include<iostream>
- #include<malloc.h>
- using namespace std;
- int main()
- {
- clock_t start,finish;
- double duration;
- int n, *w;
- char ifile[50];
- char compress_file[50];
- char decompress_file[50];
- char key;
- HuffmanTree HT = NULL;
- HuffmanPCode HC = NULL;
- do
- {
- system("cls");
- xj_Interface();
- cin>>key;
- switch (key)
- {
- case '1':
- cout<<"请输入压缩文件路径:"<<endl;
- cin>>ifile;
- strcpy(compress_file, ifile);
- compress_file[strlen(ifile) - 4] = '\0';
- strcat(compress_file, ".huf");
- cout<<"请稍等..."<<endl;
- start=clock();
- if (!xj_Init(ifile, HC, w, n)) break;
- if (HC[n].count <= 1) break;
- xj_CreateHuffmanTree(HT, HC, w, n);
- xj_HuffmanCoding(HT, HC, n);
- xj_Compress(ifile, compress_file, HC, HT, n);
- finish=clock();
- duration=(double)(finish-start)/CLOCKS_PER_SEC;
- cout<<"压缩时间:"<<duration<<"s"<<endl;
- break;
- case '2':
- cout<<"请输入解压缩文件路径:"<<endl;
- cin>>compress_file;
- strcpy(decompress_file, compress_file);
- decompress_file[strlen(compress_file) - 4] = '\0';
- strcat(decompress_file, ".txt");
- cout<<"请稍等..."<<endl;
- start=clock();
- xj_DeCompress(compress_file, decompress_file);
- finish=clock();
- duration=(double)(finish-start)/CLOCKS_PER_SEC;
- cout<<"解压缩时间:"<<duration<<"s"<<endl;
- break;
- case '3':
- break;
- default :
- cout<<"输入编号错误"<<endl;
- break;
- }
- cout<<"是否继续<y/n>"<<endl;
- cin>>key;
- }while(key == 'y');
- cout<<"谢谢使用!"<<endl;
- return 0;
- }
四、程序运行描述以及截图
五、程序评价
此程序对大小为2.08M的文件进行压缩和解压缩测试,运行的总时间在2.3s左右,压缩后文件为1.36M。但与主流压缩软件2345好压相比还是有差距。使用2345好压压缩,压缩文件大小为679KB。所以压缩性能上还是有待提高。
几点想法:若文件进行分段压缩,效率会更好。其基本思想:若有5个字符,对应的权重w = {1, 2, 3, 4, 5},则对应的赫夫曼编码为:000、001、01、10、11。此时如果将w = {w1, w2}, w1 = {1, 2, 3}, w2 = {4, 5}。则对应的编码就变为:00、01、1、0、1。可以让频率高和频率低的字符都有较短的编码。程序实现时需要将压缩文本进行分段压缩,解压时也需要分段解压。每一段压缩文件拥有一个独立的解码表。一个解码表只对该段压缩文本进行解码。分段压缩/解压互不依赖。所以可以引入多线程技术实现并行处理,当一个进程正在向文件写入数据时,它就会主动释放所占有的CPU资源,并将自己设为等待状态。此时CPU就处于空闲状态,所以为了提高CPU的利用率,可以让其他处理进程获取空闲的CPU资源继续执行压缩/解压,提高CPU的利用率,加快编解码速度。