Cây Huffman trong nén dữ liệu

Một phần của tài liệu (LUẬN văn THẠC sĩ) các kỹ thuật kiểm thử đột biến và ứng dụng kiểm thử chương trình c (Trang 26)

Sau khi thiết lập cây Huffman ta có bảng ký tự với độ dài mã ứng với từng ký tự như sau:

B U L O H W E R

1100 1101 0010 0011 000 111 01 10

Ta nhận thấy các ký tự có tần số xuất hiện ít hơn thì sẽ có độ dài mã ngắn hơn, đối với ký tự ‘E’ và ‘R’ xuất hiện nhiều hơn có độ dài mã là 2, đối với ký tự ‘B’, ‘U’, ‘L’, ‘O’ có số lần xuất hiện ít hơn sẽ có độ dài mã là 4.

2.2.2 Mã hóa Huffman

Thuật toán nén theo Huffman được tiến hành từng bước như sau: 1) Đọc từng ký tự ở tệp nguồn.

2) Tìm ký tự đó ở nút lá của cây nhị phân

3) Duyệt từ nút lá đến nút gốc, trên đường đi thu nhận các bit 0 hoặc 1, ta được chuỗi bit của từng ký tự.

4) Đảo lại vị trí các bit của chuỗi ký tự thu được và bổ sung vào vùng đệm (nếu số bit ở vùng đệm lớn hơn hoặc bằng 8 bit ta ghi ký tự thu được vào tệp đích) 5) Lặp lại các bước trên cho đến khi hết ký tự ở tệp nguồn.

Ví dụ đối với dãy ký tự “BURROW WHEELER” sau khi nén ta được chuỗi bit sau:

11001101 10100011 11111100 00101001 00110 Như vậy thay vì lưu trữ 13-byte ASCII hết 104 bit thì với thuật tốn Huffman ta lưu trữ với 5 byte trong đó có 37 bit thực.

Sau đây là đoạn mã nén dữ liệu theo phương pháp Huffman, trong đó: - hufflist là danh sách các nút lá của cây nhị phân.

nbit = 0; ch = 0; int k, t=0;

while ((c=getc(IN)) != EOF ) { node = hufflist[c]; if (!node) return; k=0; while( node->parent){ k++; if ( node == node->parent->child_1 ){ t = (t << 1) | 0; } if ( node == node->parent->child_2 ){

t = (t << 1) | 1; } node = node->parent; } while (k--){ nbit++; ch = (ch << 1) | (t & 1); t = t >> 1; if(nbit==8){ putc(ch, OUT); nbit = 0; } } } 2.2.3 Giải mã Huffman

Thuật toán giải nén được thực thi như sau: 1) Đọc từng ký tự ở tệp nguồn

2) Duyệt từ gốc của cây tới nút lá căn cứ vào giá trị bit đọc được từ ký tự (nếu bit có trị 1 ta duyệt tiếp con bên phải, nếu bit có trị 0 ta duyệt tiếp con bên trái)

3) Ghi giá trị nút lá vào tệp đích

4) Lặp lại các bước trên cho tới khi hết tệp nguồn.

Đối với ví dụ trên (hình 8), với chuỗi bit vào là:

11001101 10100011 11111100 00101001 00110

Ta lần lượt đọc từng bit và duyệt từ gốc của cây Huffman tới nút lá như sau: - đọc bit 1 11001101 10100011 11111100 00101001 00110

- đi tới nút  của cây nhị phân

- đi tới nút  của cây nhị phân

- đọc tiếp bit 0 11001101 10100011 11111100 00101001 00110 - đi tới nút  của cây nhị phân

- đọc tiếp bit 0 11001101 10100011 11111100 00101001 00110

- đi tới nút B là nút lá của cây nhị phân, ghi giá trị B vào tệp đích, quay trở lại gốc và đọc bit tiếp theo. Quá trình cứ tiếp tục cho đến khi đọc hết các bit trong tệp nguồn.

Sau đây là đoạn mã giải nén dữ liệu theo phương pháp Huffman, trong đó top là gốc của cây nhị phân.

node=top;

while ( (c=getc(IN)) != EOF ) { for(nbit=7; nbit>=0; ){

if( node->child_1 && node->child_2 ) { if ((( c >> nbit) & 1) != 0 ) { node = node->child_2; } else { node = node->child_1; } nbit--; } else{ putc(node->ch, OUT); node = top; } } }

2.3 Thuật toán nén số học

2.3.1 Giới thiệu nén số học

Ý tưởng của thuật toán nén số học dựa vào xác suất tích lũy các ký hiệu liên tiếp tương ứng với mỗi đoạn con trong đoạn [0,1). Một cách cụ thể là những đoạn con [0,1) được dùng để biểu diễn những ký hiệu của mã. Ký hiệu thứ nhất của một khối dữ liệu được mã hóa bằng cách chọn một đoạn con tương ứng. Chiều dài của đoạn con được chọn bằng xác suất mong muốn xuất hiện. Những ký hiệu tiếp theo được mã hóa bằng cách mở rộng đoạn con được chọn thành một khoảng đơn vị và chọn một đoạn con tương ứng.

Ví dụ. Xét bảng mã nhị phân (A, B) với xác suất (PA, PB) được phân bố tương ứng là (0.2, 0.8).

Khởi tạo đoạn [0,1) như hình 9(a)

Giả sử rằng chuỗi ký hiệu đầu vào chỉ chứa 1 ký hiệu. Khi đó đoạn [0,1) được chia thành hai đoạn con tương ứng với phân bố xác suất hai ký hiệu A, B là PA, PB như hình 9(b).

Giả sử chuối ký hiệu đầu vào chứa hai ký hiệu, mỗi đoạn con lại được chia thành hai đoạn con tương ứng cho hai ký hiệu như hình 9(c).

Tương tự ta có thể mở rộng bảng mã nhị phân, khi đó mỗi chuỗi có mối quan hệ một-một với mỗi đoạn con duy nhất như bảng

Chuỗi Xác suất Phân bố xác suất Đoạn con AA PA × PA 1/16 [0, 1/16) AB PA × PB 1/4 [1/16, 1/4) BA PB × PA 1/4 [1/4, 7/16) BB PB × PB 7/16 [7/16, 1)

Bảng 3. Bảng mã nhị phân mở rộng với hai ký hiệu

Tuy nhiên, không cần phải mở rộng bảng mã nhị phân và tính tốn xác suất mỗi khi kết hợp ký hiệu. Giả sử ta có chuỗi “ABBBAABAAA” gồm 10 ký hiệu với sự phân bố xác suất (A, B) tương ứng là (0.2, 0.8). Đầu tiên thay vì mở rộng bảng mã cần phải tới 210 phần tử thì ta tính xác suất tích lũy của ABBBAABAAA tương ứng như sau 0.2 × 0.8 × 0.8 × 0.8 × 0.2 × 0.2 × 0.8 × 0.2 × 0.2 × 0.2 = 0.000026214. Cuối cùng kết quả 0.000026214 được dùng để mã hóa và giải mã. Các ký hiệu tiếp theo ta cần tính cận trên (high) và cận dưới (low) của phân đoạn các ký hiệu.

Kết quả đầu ra của thuật toán nén số học là một số thực trong khoảng [0, 1). Số thực này là số duy nhất để giải mã chính xác chuỗi ký hiệu được mã hóa.

Thuật tốn nén số học vượt qua các rào cản đối với thuật toán Huffman. Thuật tốn này mã hóa một chuỗi các ký hiệu liên tiếp thay vì mã hóa từng ký hiệu đơn. Điều này làm giảm sự khác nhau giữa entropy và độ dài trung bình của các ký hiệu. Thuật tốn nén số học (tĩnh) không đưa ra bất cứ một từ mã nào cho tới khi xét tới toàn bộ khối dữ liệu đầu vào.

2.3.2 Mơ hình nén số học

Mơ hình nén số học cũng tương tự như mơ hình nén Huffman. Nó dựa trên bảng ký hiệu và sự phân bố xác suất của các ký hiệu. Xác suất các ký hiệu được tính

tuần tự chính xác từ dữ liệu nguồn, yêu cầu này cần phải đọc toàn bộ tệp nguồn trước khi mã hóa.

Giống như nén Huffman, nén số học cũng có hai mơ hình đó là dạng tĩnh và dạng động. Mơ hình động là ta tính xác suất của các ký hiệu ngay trong quá trình xử lý, nghĩa là khơng phải đọc tồn bộ tệp tin để biết xác suất của các ký tự rồi mới xử lý nén dữ liệu. Trong luận văn này tác giả trình bày thuật toán nén số học dạng tĩnh.

Sau khi đã chia các đoạn cho các ký hiệu, ta sẽ đọc từng ký hiệu trong tệp nguồn, mỗi lần đọc một phân đoạn con mới được nhận ra theo xác suất của ký hiệu nhập vào. Ta thiết lập phân đoạn khởi đầu là [0,1), quá trình này được lặp lại cho đến khi kết thúc chuỗi ký hiệu. Sau đó phương pháp nén số học kết xuất một số thực bên trong phân đoạn con cuối cùng của khối đang xét.

Ký hiệu đoạn khởi đầu là [L, L + d) trong đó biến L lưu trữ giá trị thấp nhất của phân đoạn và d lưu trữ khoảng cách giữa giá trị thấp nhất và giá trị cao nhất của phân đoạn. Các bước sau mô tả điều này:

1. Đặt phân đoạn khở đầu là [0, 1)

2. Lặp lại các bước sau cho tới khi kết thúc khối dữ liệu vào 2.1. Đọc ký hiệu s tiếp theo trong khối dữ liệu vào

2.2. Chia đoạn hiện tại thành các đoạn con sao cho kích thước của chúng tương ứng với xác suất của các ký hiệu.

2.3. Cập nhật phân đoạn hiện tại ứng với ký hiệu s.

3. Khi kết thúc khối dữ liệu vào, kết xuất số thực trong phân đoạn hiện tại. Hình 10 minh họa phân đoạn với khối dữ liệu “IOU”, kết thúc quá trình xử lý này, thuật toán kết xuất số thực trong phân đoạn [0.37630, 0.37819).

Hình 10. Minh họa phân đoạn với khối dữ liệu “IOU”

Mã số học áp dụng đối với 2 ký hiệu

Đặt P1 là xác suất của ký hiệu s1 và P2 là xác suất của ký hiệu s2, trong đó P2=1-P1. Đặt phân đoạn hiện tại là [L, L+d) và kết xuất phân số x thỏa mãn L ≤ x < L+d.

Khởi tạo L=0, d=1. Nếu ký hiệu tiếp theo hoặc là s1 hoặc là s2 ứng với xác suất P1 hoặc là P2, khi đó ta định nghĩa các phân đoạn và lựa chọn phân đoạn thích hợp:

[L, L+d × P1) và [L+d × P1, L+d × P1 + d × P2) Chú ý là L+d × P1 + d × P2 = L + d (P1 + P2) = L + d.

Thuật toán nén như sau: 1: L = 0, d = 1

2: Đọc ký tự tiếp theo 3: If ký tự là s1 then

4: d = d × P1 5: Else

6: L = L + d × P1; d = d × P2 7: End if

8: If hết ký hiệu then

9: kết xuất phân số trong khoảng [L, L + d) 10: Else

11: goto bước 2. 12: end if

Giải mã số học áp dụng đối với 2 ký hiệu

Đặt P1 là xác suất của ký hiệu s1 và P2 là xác suất của ký hiệu s2, trong đó P2=1-P1. Cho phân số x trong khoảng [0, 1) và độ dài của khối dữ liệu nguồn, khối dữ liệu nguồn có thể được giải mã như sau:

1: L = 0, d = 1 2: Đọc phân số x

3: If x thuộc phân đoạn [L, L + d × P1) then 4: Kết xuất s1; d = d × P1

5: Else

6: Kết xuất s2; L = L + d × P1; d = d × P2 7: End if

8: if numberOfDecodedSymbols < requiredNumberOfSyrnbols then 9: goto bước 2

10: end if

2.3.3 Trường hợp tổng quát trong nén số học

Đặt S = (s1, s2, …, sn) là các ký hiệu của khối dữ liệu nguồn tương ứng với sự phân bố xác suất xảy ra của các ký hiệu là P = (p1, p2, …, pn). Các đoạn con đối với

Ví dụ, sau lần lặp đầu tiên, khởi tạo đoạn lặp [0,1) được suy ra thành n đoạn con như sau:

[0, p1) [p1, p1 + p2) [p1+ p2, p1 + p2+ p3) …. [p1+ p2+…+ pn-1, p1 + p2+…+ pn-1+ pn) Trong đó p1 + p2+…+ pn-1+ pn =1.

Nếu ký hiệu đầu tiên đọc được có vị trí thứ i (ký tự si) trong bảng mã, khi đó giới hạn dưới (low) của phân đoạn con được tích lũy xác suất là pi = p1 + p2 + … + pi và giới hạn trên (high) là low + pi.

Độ dài của phân đoạn là high – low được sử dụng để tính cho các phân đoạn khác trong những lần lặp tiếp theo:

[low, low + (high – low) * p1)

[low + (high – low) * p1, low + (high – low) * p2) …

[low + (high – low) * pn-1, low + (high – low) * pn) Trong đó xác suất tích lũy của cả tập hợp là Pn = p1 + p2 + … + pn = 1.

Thuật toán nén tổng quát như sau:

1: low = 0.0; high = 1.0;

2: while còn ký hiệu đầu vào do 3: đọc ký hiệu s

4: codeRange = high – low

5: high = low + codeRange * high_range(s) 6: low = low + codeRange * low_range(s) 7: end while

Thuật toán giải nén tổng quát

1: đọc số mã hóa x 2: repeat

3: tìm ký hiệu có phân đoạn bao trùm x 4: kết xuất ký hiệu s

5: codeRange = high_range(s) – low_range(s) 6: x = (x – low_range(s))/codeRange

7: until hết ký hiệu

2.3.4. Ví dụ về nén số học

Cho chuỗi ký hiệu “BILL GATES” với xác suất của các ký hiệu được phân bố như sau, ta có thể tính phân đoạn cho từng ký hiệu:

Ký hiệu Xác suất Phân đoạn low high Space 1/10 0.00 0.10 A 1/10 0.10 0.20 B 1/10 0.20 0.30 E 1/10 0.30 0.40 G 1/10 0.40 0.50 I 1/10 0.50 0.60 L 2/10 0.60 0.80 S 1/10 0.80 0.90 T 1/10 0.90 1.00

Bảng 4. Bảng xác suất và phân đoạn của các ký hiệu

Sau khi áp dụng thuật toán nén tổng quát ta tìm được cận dưới và cận trên cho chuỗi ký hiệu “BILL GATES” như sau:

Đọc ký hiệu low high B 0.2 0.3 I 0.25 0.26 L 0.256 0.258 L 0.2572 0.2576 SPACE 0.25720 0.25724 G 0.257216 0.257220 A 0.2572164 0.2572168 T 0.25721676 0.2572168 E 0.257216772 0.257216776 S 0.2572167752 0.2572167756

Bảng 5. Mã hóa theo phương pháp số học

Cuối cùng ta cho số thực 0.2572167752 là số có thể giải mã được chuỗi ký hiệu gốc. Áp dụng thuật toán giải nén tổng quát, ta nhận thấy số 0.2572167752 nằm trong phạm vi 0.2 đến 0.3, và ta tìm được ký hiệu “B”. Sau đó ta tính số thực tiếp theo áp dụng thuật toán giải nén tổng quát ta được 0.572167752, số thực này nằm trong phạm vi 0.5 đến 0.6 và ta tìm được ký hiệu “I”. Quá trình cứ tiếp tục như vậy, ta có bảng thống kê sau:

Đọc số mã Kết xuất

ký hiệu low high codeRange

0.2572167752 B 0.2 0.3 0.1 0.572167752 I 0.5 0.6 0.1 0.72167752 L 0.6 0.8 0.2 0.6083876 L 0.6 0.8 0.2 0.041938 SPACE 0.0 0.1 0.1 0.41938 G 0.4 0.5 0.1 0.1938 A 0.1 0.2 0.1 0.938 T 0.9 1.0 0.1 0.38 E 0.3 0.4 0.1 0.8 S 0.8 0.9 0.1 0.0

2.4 Phép biến đổi Burrows-Wheeler (BWT)

2.4.1 Giới thiệu BWT

Phép biến đổi Burrow-Wheeler không phải là một thuật toán nén dữ liệu, mà chỉ làm thay đổi cấu trúc dữ liệu, tạo dữ liệu đầu vào cho các thuật toán nén dữ liệu khác như RLE, hoặc có thể kết hợp với phương pháp MTF để đạt được hiệu quả nén cao hơn với các thuật tốn nén Entropy.

Mục đích của BWT là nhóm các ký tự giống nhau ở gần nhau hơn, khối ký tự mới nhận được khi nén sẽ hiệu quả hơn, để thực hiện được điều này ta lấy ví dụ trên chuỗi ký tự S như sau:

Cho chuỗi ký tự S = “NUNANUNONG”, để đơn giản ta khơng xét đến dấu cách.

2.4.2 Mã hóa

1) Tạo ra các khối dữ liệu bằng cách hốn vị vịng trịn ký tự cuối cùng của khối lên đầu khối. Ta thu được ma trận X[n×n], mỗi hàng của ma trận là một hoán vị của S: X 0 1 2 3 4 5 6 7 8 9 0 N U N A N U N O N G 1 G N U N A N U N O N 2 N G N U N A N U N O 3 O N G N U N A N U N 4 N O N G N U N A N U 5 U N O N G N U N A N 6 N U N O N G N U N A 7 A N U N O N G N U N 8 N A N U N O N G N U 9 U N A N U N O N G N 2) Sắp xếp các dịng ma trận theo thứ tự tăng dần, ta có X 0 1 2 3 4 5 6 7 8 9 0 A N U N O N G N U N 1 G N U N A N U N O N 2 N A N U N O N G N U 3 N G N U N A N U N O

4 N O N G N U N A N U 5 N U N A N U N O N G 6 N U N O N G N U N A 7 O N G N U N A N U N 8 U N A N U N O N G N 9 U N O N G N U N A N

3) Tạo ra khối dữ liệu mới L là cột cuối cùng của ma trận. Giá trị D lưu trữ vị

trí ký tự đầu tiên của S, D là nguồn gốc để khôi phục L thành S

X F L 0 A N U N O N G N U N 1 G N U N A N U N O N 2 N A N U N O N G N U 3 N G N U N A N U N O 4 N O N G N U N A N U 5 N U N A N U N O N G 6 N U N O N G N U N A 7 O N G N U N A N U N 8 U N A N U N O N G N D 9 U N O N G N U N A N Nhận xét:

- L có thể được lưu trữ trong mảng một chiều:

0 1 2 3 4 5 6 7 8 9

L= N N U O U G A N N N

- Tương tự với F cũng là mảng một chiều nhận giá trị là cột đầu tiên của ma trận,

F được suy ra từ mảng L bằng cách sắp xếp L tăng dần:

0 1 2 3 4 5 6 7 8 9

F= A G N N N N N O U U

void bwt_encode ( FILE *in, FILE *out )

{ unsigned int i = 0, last_index = BWT_SIZE; while( 1 ) {

/* get bytes and save to buffer. */

nread=fread((unsigned char *)bwt_buf, 1, BWT_SIZE, in); if ( nread == 0 ) break;

/* write the size of the block read. */

fwrite( &nread, sizeof( unsigned int ), 1, out ); /* initialize index table. */

for ( i = 0; i < nread; i++ ) { index_table[i] = i;

}

/* sort the bwt buffer array; */

qsort( index_table, nread, sizeof(int),

(int (*)(const void *, const void *))bwt_comp); /* output Last column buffer. */

last_index = nread;

for ( i = 0; i < nread; i++ ){ if ( index_table[i] == 0 ){

putc( bwt_buf[ nread-1 ], out ); last_index = i;

}

else putc( bwt_buf[ index_table[i] - 1], out ); }

fprintf(stderr,"\n-[last index: %5d]- ", last_index); /* output index of the original string in Last. */ fwrite( &last_index, sizeof(unsigned int), 1, out ); }

}

int bwt_comp( unsigned int *a, unsigned int *b ) { register int a1 = *a, b1 = *b, tmp = a1;

if ( bwt_buf[a1] != bwt_buf[b1] ) {

if ( bwt_buf[a1] > bwt_buf[b1] ) return 1; else return -1;

} else {

!= bwt_buf[++b1 == nread? b1 = 0 : b1] ){ if (bwt_buf[a1] > bwt_buf[b1]) return 1; else return -1; } } while ( a1 != tmp ); } return 0; } 2.4.3 Giải mã

Mã hóa bằng BWT cho ta mảng L và giá trị D. Từ L ta có thể suy ra được F. Để có thể suy từ L ra S, ta phải tìm mối quan hệ giữa L và F. Với mỗi ký tự trong L

Một phần của tài liệu (LUẬN văn THẠC sĩ) các kỹ thuật kiểm thử đột biến và ứng dụng kiểm thử chương trình c (Trang 26)

Tải bản đầy đủ (PDF)

(63 trang)