• No results found

FPC - Mảng động và một số ứng dụng

N/A
N/A
Protected

Academic year: 2021

Share "FPC - Mảng động và một số ứng dụng"

Copied!
13
0
0

Loading.... (view fulltext now)

Full text

(1)

1. Mở đầu

Đối tượng đọc tài liệu: Bất kỳ ai muốn tìm hiểu về Free Pascal ở nội dung như tiêu đề đã chỉ ra. Nhưng nếu bạn là một học sinh, sinh viên hay nói chung là những người chỉ dùng Free Pascal để nghiên cứu thuật toán, tôi không nghĩ rằng tài liệu này giúp gì nhiều.

Nội dung tài liệu: Giới thiệu cấu trúc mảng động và một số ứng dụng.

Giới hạn trách nhiệm: Tôi không phải một Pascal guru thứ thiệt trong cộng đồng những người sử dụng Free Pascal, hơn thế nữa tài liệu này viết với mục đích giải trí, vì vậy không thể đảm bảo nội dung trong tài liệu này chính xác tuyệt đối. Người đọc cần tự chịu trách nhiệm với nội dung của tài liệu. Nếu lập trình theo tài liệu này mà CPU của bạn bốc cháy hoặc bản thân bạn bị tẩu hoả nhập ma, ấy là lỗi tại bạn.

Bản quyền: Tài liệu này phát hành theo giấy phép GNU/FDL, nghĩa là bạn toàn quyền sử dụng, sao chép, sửa chữa và phân phối lại tài liệu miễn sao tên tác giả (nqhien@gmail.com) không bị thay thế và khi phân phối lại tài liệu này, bạn cũng phải tuân theo giấy phép GNU/FDL.

Môi trường Free Pascal: Chương trình dịch: Free Pascal Compiler 2.6.0, compiler mode: Free Pascal dialect, IDE được khuyến khích sử dụng là Free Pascal IDE.

2. Mảng động

Ví dụ 1: type AI = array of Integer; var a: AI; i: Integer; BEGIN SetLength(a, 10);

for i := Low(a) to High(a) do a[i] := i;

for i := Low(a) to High(a) do

Write('a[',i,']','=',a[i], ', ');

WriteLn;

SetLength(a, 5);

for i := Low(a) to High(a) do

(2)

WriteLn;

SetLength(a, 10);

for i := Low(a) to High(a) do

Write('a[',i,']','=',a[i], ', '); WriteLn; END. CTRL+F9 a[0]=0, a[1]=1, a[2]=2, a[3]=3, a[4]=4, a[5]=5, a[6]=6, a[7]=7, a[8]=8, a[9]=9 a[0]=0, a[1]=1, a[2]=2, a[3]=3, a[4]=4, a[0]=0, a[1]=1, a[2]=2, a[3]=3, a[4]=4, a[5]=0, a[6]=0, a[7]=0, a[8]=0, a[9]=0 Từ ví dụ, ta rút ra các nhận xét:

Mảng động (dynamic array) không cần khai báo trước miền chỉ số cũng như số lượng phần tử : AI = array of Integer;.

Miền chỉ số của mảng động a: Low(a)..High(a), trong đó Low(a) = 0, High(a) = Length(a)-1, Length là hàm trả lại số phần tử của một mảng động. Mảng động được cấp phát bộ nhớ (trong thời gian chạy) bằng thủ tục SetLength.

o Mảng động khi bị SetLength với số phần tử ít hơn, các phần tử ở cuối mảng bị xoá và các phần tử đầu có giá trị như cũ.

o Mảng động khi bị SetLength với số phần tử nhiều hơn, giá trị các phần tử đầu cũng được giữ nguyên và các phần tử mới có trị ngẫu nhiên nào đó (không có gì đảm bảo các phần tử này là 0 như ví dụ trên). Ví dụ 2: var a, b: AI; i: Integer; BEGIN SetLength(a, 10);

for i := Low(a) to High(a) do a[i] := i;

b := a;

b[Length(b) shr 1] := -1;

for i := Low(a) to High(a) do

Write('a[',i,']','=',a[i], ', ');

WriteLn;

for i := Low(b) to High(b) do

Write('b[',i,']','=',b[i], ', '); WriteLn; END. CTRL+F9 a[0]=0, a[1]=1, a[2]=2, a[3]=3, a[4]=4, a[5]=-1, a[6]=6, a[7]=7, a[8]=8, a[9]=9 b[0]=0, b[1]=1, b[2]=2, b[3]=3, b[4]=4, b[5]=-1, b[6]=6, b[7]=7, b[8]=8, b[9]=9 Nhận xét: Phép gán hai mảng động không giống với gán 2 mảng tĩnh, phép gán b := a;

các phần tử của a không được copy sang b. Hai mảng a và b sau phép gán chỉ là một, việc thay đổi phần tử của b đồng nghĩa với việc thay đổi phần tử tương ứng của a. Nói cách khác: sau phép gán b := a; thì a và b cùng tham chiếu (reference) đến một vùng nhớ.

(3)

Thực chất mảng động là con trỏ (pointer) và SetLength là thủ tục cấp phát, thu hồi vùng nhớ cho con trỏ đó, bộ nhớ được sử dụng để lưu trữ mảng động là heap. Tuy nhiên mảng động được quản lý khác con trỏ ở chỗ:

Về cú pháp, mảng động cho phép dùng các toán tử [] và khai báo tự nhiên, giống như mảng tĩnh (static array).

Vùng nhớ được cấp phát cho mảng động được quản lý theo cơ chế đếm số tham chiếu (reference count), nghĩa là vùng nhớ này sẽ được tự động giải phóng nếu số lượng tham chiếu bằng 0. Ví dụ 3: Với a, b là 2 mảng động, xét đoạn lệnh: SetLength(a, 10); b := a; SetLength(a, 0); //1 SetLength(b, 0); //2

Trước lệnh 1, số tham chiếu đến vùng nhớ đã cấp phát cho a là 2. Sau lệnh gán 1, số tham chiếu đến vùng nhớ này là 1 vì a có số phần tử bằng 0. Sau lệnh 2, b cũng có số phần tử bằng 0. Chú ý rằng ta có thể viết b := nil; thay cho SetLength(b, 0);

Điểm cuối cùng đề cập đến trong phần này bàn về mảng động nhiều chiều, xem ví dụ sau: Ví dụ 4:

type

AI = array of Integer;

AII = array of AI; // array of array of Integer;

var a: AII; i, j: Integer; BEGIN SetLength(a, 10); for i := 0 to High(a) do SetLength(a[i], i+1); END.

Trong ví dụ này, a: AII là một mảng động hai chiều, dễ thấy dòng 0 có 1 phần tử, dòng 1 có 2 phần tử, dòng i có i+1 phần tử. Ở đây ta thấy mỗi dòng của mảng hai chiều có thể có số phần tử khác nhau.

Đến đây ta có thể phát biểu về mảng động: Mảng động là cấu trúc dữ liệu kiểu mảng với số phần tử là “động” có thể thay đổi trong thời gian chạy (run time). Làm việc với mảng động giống như làm việc với mảng tĩnh nhưng mảng động thực chất là con trỏ, vùng nhớ được cấp phát cho mảng động nằm trên heap và được Free Pascal quản lý theo cách riêng.

(4)

3. Một số phép toán thường dùng trên mảng động

Ngoài High, Low, Length, SetLength các hàm, thủ tục sau đây thường được dùng trên mảng động:

1. Copy – Đây là hàm giống hệt hàm copy trên String hay AnsiString.

2. SizeOf – Hàm SizeOf một biến mảng động luôn trả lại giá trị là kích thước (đơn vị byte) của một con trỏ (luôn là 4 byte trong môi trường 32 bit). Để lấy kích thước của mảng động phải kết hợp Length và SizeOf kiểu phần tử của mảng động đó.

3. Move – Move vẫn có thể dùng bình thường trên mảng động, nhưng nhớ rằng biến mảng động là con trỏ, vì vậy trong thủ tục move tránh truy xuất đến tên mảng, chẳng lệnh sau không có vấn đề gì với mảng tĩnh move(a, a[5], Length(a)*SizeOf(a[0])); vì với mảng tĩnh a và a[0] cùng địa chỉ nhưng với mảng động cần sửa lại: move(a[0], a[5],

Length(a)*SizeOf(a[0])); hoặc move(a^, a[5], Length(a)*SizeOf(a[0]));

4. Fill – Các thủ thục FillByte, FillChar, FillDWord, FillQWord, FillWord cũng có thể dùng với lưu ý như khi dùng move.

4. Khảo sát tốc độ mảng động

Ta dùng chương trình sau để đo tốc độ của mảng động ở các thao tác: truy xuất đến phần tử, lấy số phần tử và SetLength và so sánh với các thao tác trên mảng tĩnh.

{Mode: Release} uses SysUtils; const MAX = 10000; COUNT = 10000000; type AI = array of Integer;

SI = array[0..MAX-1] of Integer;

TProc = procedure;

var

a: AI;

b: SI;

n: LongInt;

procedure SpeedMeasure(Title: string; proc: TProc; count: LongInt);

var

time: TDateTime;

begin

time := now;

Write(Title,'...');

for count := count downto 1 do proc;

(5)

WriteLn(FormatDateTime('nn:ss:zz', time)); end; procedure AIAccess; begin a[0] := a[1]; end; procedure SIAccess; begin b[0] := b[1]; end; procedure AISetLength; begin Inc(n); SetLength(a, n); end; procedure SILength; begin n := MAX; end; procedure AILength; begin n := Length(a); end; BEGIN SetLength(a, MAX);

SpeedMeasure('Static array access ', @SIAccess, COUNT);

SpeedMeasure('Static array Length ', @SILength, COUNT);

SpeedMeasure('Dynamic array access ', @AIAccess, COUNT);

SpeedMeasure('Dynamic array Length ', @AILength, COUNT);

n := 0;

SpeedMeasure('Dynamic array SetLength', @AISetLength, COUNT);

END. CTRL+F9

Static array access ...00:00:047 Static array Length ...00:00:047 Dynamic array access ...00:00:062 Dynamic array Length ...00:00:078 Dynamic array SetLength...00:05:485

Nhận xét:

Tốc độ truy xuất phần tử trên mảng động chậm hơn so với mảng tĩnh một chút.

Việc gọi đến hàm Length để lấy số phần tử của mảng đòi hỏi chi phí gọi hàm. Tương tự như vậy khi sử dụng High và Low.

SetLength có tốc độ rất chậm vì đây là thủ tục cấp phát vùng nhớ. Tương tự, có thể thử để thấy việc giải phóng vùng nhớ cũng rất chậm.

(6)

5. Gỡ lỗi với mảng động

Trình gỡ lỗi được sử dụng ở đây là GDB – trình gỡ lỗi mặc định, tích hợp trong IDE Free Pascal. Với mảng động, đáng tiếc là ta không dễ dàng gỡ lỗi như đối với mảng tĩnh. Không thể theo dõi (watch) toàn bộ nội dung của một mảng động giống như với mảng tĩnh, thay vào đó chỉ có thể theo dõi giá trị từng phần tử. Giả sử để theo dõi giá trị của phần tử tứ i của mảng động a, cần thêm biểu thức theo dõi (add watch – CTRL+F7 trong IDE Free Pascal): a^[i].

Mẹo nhỏ để theo dõi một đoạn giá trị của mảng động là dùng kỹ thuật ép kiểu trong GDB. Ta sẽ ép kiểu mảng động về một mảng tĩnh. Chẳng hạn trong chương trình ở phần 4, để theo dõi đoạn MAX phần tử đầu tiên của mảng động a, ta có thể thêm biểu thức theo dõi: SI(a).

6. Cài đặt một số cấu trúc dữ liệu bằng mảng động.

Ta sẽ cài đặt (implement) một số cấu trúc dữ liệu như stack, queue theo hai cách: truyền thống bằng con trỏ và cách mới: sử dụng mảng động, qua đó so sánh và rút ra một số nhận xét.

Ví dụ 1: Cấu trúc dữ liệu stack với TAIStack là stack dùng mảng động và TLkStack là stack cài đặt bằng con trỏ liên kết. Ta sẽ cài đặt 3 thao tác chính: push, pop và top cho stack.

uses

SysUtils;

const

COUNT = 10000000;

type

TAIStack = array of LongInt;

PNode = ^TNode; TNode = record x: LongInt; next: PNode; end; TLkStack = PNode; TProc = procedure; var a: TAIStack; b: TLkStack; {TAIStack}

procedure init(var stack: TAIStack);

begin

stack := nil;

end;

(7)

begin

SetLength(stack, Length(stack) + 1);

stack[High(stack)] := x;

end;

procedure pop(var stack: TAIStack);

begin

SetLength(stack, High(stack));

end;

function top(var stack: TAIStack): LongInt;

begin

top := stack[High(stack)]

end;

{TLkStack}

procedure init(var stack: TLkStack);

begin

stack := nil;

end;

procedure push(var stack: TLkStack; x: LongInt);

var p: PNode; begin p := stack; new(stack); stack^.next := p; stack^.x := x; end;

procedure pop(var stack: TLkStack);

var

p: PNode;

begin

p := stack;

stack := stack^.next;

dispose(p);

end;

function top(var stack: TLkStack): LongInt;

begin

top := stack^.x;

end;

{Measure}

procedure SpeedMeasure(Title: string; proc: TProc; count: LongInt);

var

time: TDateTime;

begin

time := now;

Write(Title,'...');

for count := count downto 1 do proc;

(8)

WriteLn(FormatDateTime('nn:ss:zz', time));

end;

procedure AIPush; begin push(a, 0); end;

procedure AIPop; begin pop(a); end;

procedure AITop; var tmp: LongInt; begin tmp := top(a); end;

procedure LkPush; begin push(b, 0); end;

procedure LkPop; begin pop(b); end;

procedure LkTop; var tmp: LongInt; begin tmp := top(b); end; {main} BEGIN init(a); init(b);

SpeedMeasure('AI push', @AIPush, COUNT);

SpeedMeasure('AI top ', @AITop, COUNT);

SpeedMeasure('AI pop ', @AIPop, COUNT);

SpeedMeasure('Lk push', @LkPush, COUNT);

SpeedMeasure('Lk top ', @LkTop, COUNT);

SpeedMeasure('Lk pop ', @LkPop, COUNT);

END. CTRL+F9 AI push...00:20:297 AI top ...00:00:140 AI pop ...00:01:031 Lk push...00:00:781 Lk top ...00:00:094 Lk pop ...00:00:516

Tốc độ của TAIStack rõ ràng không có gì ấn tượng so với TLkStack nếu không muốn nói là thảm hại. Nguyên nhân có thể thấy là do tốc độ chậm chạp của SetLength trong các thao tác push và pop.

Để khắc phục sự chậm chạp của SetLength ta sẽ cố gắng cải tiến sao cho số lần phải gọi đến SetLength càng ít càng tốt.

(9)

Trong thao tác push, từ lệnh SetLength(stack, Length(stack) + 1); ta có một hướng cải tiến là mỗi lần SetLength cho mảng động ta sẽ dùng SetLength(stack, Length(stack) +

k); với k > 1. Ta cần dùng một biến n để quản lý số lượng phần tử thực tế trong stack, và chỉ SetLength cho mảng động khi n vượt quá Length hiện tại của mảng động. Số k được chọn càng lớn càng tốt, tuy nhiên ở đây ta chọn k bằng 2 lần Length hiện tại của mảng động để đảm bảo bộ nhớ không quá lãng phí.

Đối với thao tác pop, tương tự như push, nếu như cần tiết kiệm bộ nhớ ta có thể SetLength lại mỗi khi n nhỏ hơn hai lần Length hiện tại của mảng động, nhưng nếu ưu tiên cho tốc độ ta có thể bỏ qua việc giải phóng vùng nhớ.

Với cách làm như trên, để tối ưu chi phí thời gian ta đã tốn gấp 2 lần chi phí bộ nhớ. Định nghĩa lại kiểu dữ liệu TAIStack:

TAIStack = record n: LongInt;

x: array of LongInt;

end;

Cài đặt các thao tác trên TAIStack:

{TAIStack}

procedure init(var stack: TAIStack);

begin

stack.n := 0;

stack.x := nil;

end;

procedure push(var stack: TAIStack; x: LongInt);

begin

Inc(stack.n);

if stack.n > Length(stack.x) then SetLength(stack.x, stack.n shl 1);

stack.x[stack.n-1] := x;

end;

procedure pop(var stack: TAIStack);

begin

Dec(stack.n);

//if stack.n < Length(stack.x) shr 1 then

//SetLength(stack.x, Length(stack.x) shr 1);

end;

function top(var stack: TAIStack): LongInt;

begin

top := stack.x[stack.n - 1]

end;

Kết quả sau khi cải tiến: AI push...00:00:360 AI top ...00:00:109

(10)

AI pop ...00:00:109 Lk push...00:00:735 Lk top ...00:00:094 Lk pop ...00:00:515

Tốc độ đạt được rất đáng khích lệ . Tuy nhiên ta cũng có thể đoán được là TAIStack không thể đạt được tốc độ như stack sử dụng mảng tĩnh.

Lưu ý rằng tài liệu này không khuyến khích người lập trình chọn cách cài đặt những cấu trúc dữ liệu như stack nói trên bằng mảng động thay cho cách dùng con trỏ liên kết truyền thống. Khi lập trình, người lập trình phải tuỳ cơ ứng biến tuỳ theo bài toán cụ thể.

Ví dụ 2: Cấu trúc dữ liệu queue: TAIQueue cài đặt bằng mảng động. type

AI = array of LongInt;

TAIQueue = record

first, last: LongInt;

x: AI;

end;

TProc = procedure;

var

a: TAIQueue;

procedure init(var queue: TAIQueue);

begin

queue.x := nil;

queue.first := 0;

queue.last := -1;

end;

procedure push(var queue: TAIQueue; x: LongInt);

var

tmp: AI;

begin

Inc(queue.last);

if queue.last = Length(queue.x) then

SetLength(queue.x, (queue.last+1) shl 1);

queue.x[queue.last] := x;

end;

procedure pop(var queue: TAIQueue);

begin

Inc(queue.first);

if queue.first > Length(queue.x) shr 1 then begin

Dec(queue.last, queue.first);

Move(queue.x[queue.first], queue.x[0],

(queue.last+1)*SizeOf(queue.x[0])

);

queue.first := 0;

end;

(11)

function top(var queue: TAIQueue): LongInt;

begin

top := queue.x[queue.first];

end;

procedure SpeedMeasure(Title: string; proc: TProc; count: LongInt);

var

time: TDateTime;

begin

time := now;

Write(Title,'...');

for count := count downto 1 do proc;

time := now - time;

WriteLn(FormatDateTime('nn:ss:zz', time));

end;

procedure AIPush; begin push(a, 0); end;

procedure AIPop; begin pop(a); end;

procedure AITop; var tmp: LongInt; begin tmp := top(a); end; BEGIN init(a);

SpeedMeasure('AI push', @AIPush, COUNT);

SpeedMeasure('AI top ', @AITop, COUNT);

SpeedMeasure('AI pop ', @AIPop, COUNT);

END. CTRL+F9

AI push...00:00:360 AI top ...00:00:109 AI pop ...00:00:172

Queue trong ví dụ này được cài đặt tương tự stack. Thao tác pop chậm hơn trên stack vì phải xử lý thêm trường hợp: khi first quá lớn ta dịch lại first về vị trí đầu mảng.

7. Bài tập

Bài tập 1: Nếu a và b là hai biến mảng cùng kiểu, cần hoán vị a và b, khi đó nếu a và b là mảng động thì phép hoán vị sẽ nhanh hơn hay mảng tĩnh sẽ nhanh hơn?

Bài tập 2: Cho bản ghi được định nghĩa:

record

n: Integer;

(12)

end;

Với hai biến a và b thuộc kiểu bản ghi nói trên, hãy chỉ ra trường hợp mà lệnh gán a := b trở nên nguy hiểm.

Bài tập 2: Cần cài đặt cấu trúc dữ liệu stack, heap với các phần tử của stack có kích thước rất lớn, khi đó nên chọn cách cài đặt bằng con trỏ liên kết hay dùng mảng động?

Bài tập 3: Cài đặt cấu trúc dữ liệu deque bằng mảng động, viết lại các thao tác push_back, pop_back, push_front, pop_front, back, front và toán tử [] tương tự như kiểu dữ liệu deque trong thư viện STL của C++.

Bài tập 4: Thử nghiệm chương trình sau và rút ra nhận xét:

type AI = array of Integer; var a: array[1..10] of Integer; procedure foo(a: AI); begin

for i := Low(a) to High(a) do Write(a[i]);

end;

BEGIN Foo(a);

END.

Bài tập 5: Đọc chương trình dưới đây và trả lời:

Sau khi gọi foo, phần tử a[0] của biến toàn cục (global variable) a có thay đổi không? Sau khi gọi foo, biến toàn cục a có bị thay đổi không?

Khi gọi đến chương trình con foo, hệ thống sẽ cấp phát stack để lưu trữ những gì?

type AI = array of Integer; var a: AI; procedure foo(a: AI); var b: AI; begin a[0] := -1; SetLength(b, 10); a := b; end; BEGIN SetLength(a, 10); foo(a); END.

(13)

8. Lời kết

Trước khi Free Pascal hỗ trợ mảng động, để định nghĩa ra cấu trúc mảng động thông thường người lập trình phải tự chế lấy mảng động bằng con trỏ. Rất may Free Pascal đã định nghĩa sẵn kiểu dữ liệu mảng động và che dấu bản chất con trỏ của mảng động, từ đó người lập trình có thể sử dụng một cách dễ dàng và thân thiện giống như mảng tĩnh.

Mảng động cũng được hỗ trợ trong nhiều ngôn ngữ lập trình hiện đại khác như C++ với con trỏ hoặc cấu trúc dữ liệu vector (thư viện STL), Java và các ngôn ngữ .NET với kiểu dữ liệu ArrayList...

Sử dụng mảng động không đem lại lợi thế về tốc độ khi so sánh với mảng tĩnh nhưng bù lại mảng động có khả năng thay đổi số phần tử một cách linh hoạt. Nếu trong một bài toán cụ thể cần sử dụng mảng với số lượng phần tử tối đa đã xác định thì hẳn nhiên để ưu tiên tốc độ ta sẽ chọn mảng tĩnh, nhưng trong trường hợp tổng quát, chẳng hạn khi viết một thư viện định nghĩa các kiểu dữ liệu như stack, queue,… và muốn cài đặt chúng dựa trên cấu trúc mảng, khi đó mảng tĩnh không đáp ứng được yêu cầu, thay vào đó ta cần đến sự linh hoạt của mảng động.

Tài liệu này giới thiệu cấu trúc dữ liệu mảng động trong Free Pascal và minh hoạ cách sử dụng cũng như một số ứng dụng của mảng động. Toàn bộ nội dung của tài liệu, người đọc có tìm thấy trong phần tài liệu tham khảo hoặc… google, người viết tài liệu này thường dùng cách đó để tìm hiểu chứ không đọc những tài liệu như tài liệu này 

9. Tài liệu tham khảo

1. http://www.freepascal.org/docs-html/ref/ref.html 1.1. http://www.freepascal.org/docs-html/ref/refsu15.html 1.2. http://www.freepascal.org/docs-html/ref/refse15.html#x43-500003.4 2. http://wiki.freepascal.org/GDB_Debugger_Tips 3. http://en.wikipedia.org/wiki/Dynamic_array 4. http://www.cplusplus.com/reference/stl/deque/ 5. http://www.gnu.org/copyleft/fdl.html

References

Related documents

© 2013 KPMG Safi Al-Mutawa &amp; Partners, a Kuwait partnership and a member firm of the KPMG network of independent member firms affiliated with KPMG International Cooperative

Store and protect all materials to minimise saturation and contamination. Control the wetting of bricks in hot windy weather. Do not lay surface saturated bricks. Set out at

These results support hypothesis 1: Offers are significantly higher when participants are primed with a shared identity than when participants are primed with a distinct

From the forensic point of view, IaaS instances provide much more information that could be used as forensic evidence in case of an incident than the PaaS and SaaS models do [6]..

Purpose: This study aimed to: 1) describe the 4-item Early Activity Scale for Endurance (EASE) scores and Six-Minute Walk Test (6MWT) distances of children with cerebral palsy (CP)

In accordance with the provisions of Paragraph (3) of Article 21 of the Ordinance for Enforcement of the Act on the Evaluation of Chemical Substances and Regulation of

But Asian Paints, through its effective distribution management, inventory management and control of credit outstanding, in particular, managed to

What types of dyes are used for cotton, jute and flax