Giáo trình Cấu trúc dữ liệu và giải thuật - Chương 2: Ngăn xếp
Chúng ta sẽ tìm hiểu một CTDL đơn giản nhất, đó là ngăn xếp. Một cách nhất
quán như phần giới thiệu môn học đã trình bày, mỗi CTDL đều được xây dựng
theo đúng trình tự:
• Định nghĩa.
• Đặc tả.
• Phân tích các phương án hiện thực.
• Hiện thực
iệu chỉnh này, người sử dụng sẽ
không thể gây ra rác khi để một đối tượng ngăn xếp không rỗng ra khỏi tầm vực.
Destructor được khai báo như một phương thức của lớp, không có thông số và
không có trị trả về. Tên của destructor là tên lớp có thêm dấu ~ phía trước.
Stack::~Stack();
Do phương thức pop được lập trình để loại một phần tử ra khỏi ngăn xếp,
chúng ta có thể hiện thực destructor cho ngăn xếp bằng cách lặp nhiều lần việc
gọi pop:
template
Stack::~Stack() // Destructor
/*
post: ngăn xếp đã được làm rỗng.
*/
{
while (!empty())
pop();
}
Chương 2 – Ngăn xếp
Giáo trình Cấu trúc dữ liệu và Giải thuật 31
Đối với mọi cấu trúc liên kết chúng ta cần viết destructor để dọn dẹp các đối
tượng trước khi chúng ra khỏi tầm vực.
2.4.3.2. Định nghĩa lại phép gán
Ngay khi chúng ta đã bổ sung destructor cho ngăn xếp liên kết, người sử dụng
cũng có thể tạo rác khi viết vòng lặp tựa như ví dụ sau.
Stack outer_stack;
for (int i=0; i < 1000000; i++) {
Stack inner_stack;
inner_stack.push(some_data);
inner_stack = outer_stack;
}
Đầu tiên là đối tượng outer_stack được tạo ra, sau đó vòng lặp được thực hiện.
Mỗi lần lặp là một lần tạo một đối tượng inner_stack, đưa dữ liệu vào
inner_stack, gán outer_stack vào inner_stack. Lệnh gán này gây ra một vấn đề
nghiêm trọng cho hiện thực ngăn xếp liên kết của chúng ta. Thông thường, C++
thực hiện phép gán các đối tượng bằng cách chép các thuộc tính của các đối tượng.
Do đó, outer_stack.top_node được ghi đè lên inner_stack.top_node, làm cho dữ liệu
cũ tham chiếu bởi inner_stack.top_node trở thành rác. Cũng giống như phần
trước, nếu hiện thực ngăn xếp liên tục được sử dụng thì không có vấn đề gì xảy
ra. Như vậy, lỗi là do hiện thực ngăn xếp liên kết còn thiếu sót.
Hình 2.7 cho thấy tác vụ gán không được thực hiện như mong muốn. Sau phép
gán, cả hai ngăn xếp cùng chia xẻ các node. Cuối mỗi lần lặp, destructor của
inner_stack sẽ giải phóng mọi dữ liệu của outer_stack. Việc giải phóng dữ
liệu của outer_stack còn làm cho outer_stack.top_node trở thành tham
chiếu treo, có nghĩa là tham chiếu đến vùng nhớ không xác định.
Hình 2.7- Ứng dụng chép ngăn xếp.
Chương 2 – Ngăn xếp
Giáo trình Cấu trúc dữ liệu và Giải thuật 32
Vấn đề sinh ra bởi phép gán trên ngăn xếp liên kết là do nó chép các tham
chiếu chứ không phải chép các trị. Phép gán trong trường hợp này được gọi là
phép gán có ngữ nghĩa tham chiếu (reference semantics) . Ngược lại, khi phép gán
thực sự chép dữ liệu trong CTDL chúng ta gọi là phép gán có ngữ nghĩa trị (value
semantics). Trong hiện thực lớp ngăn xếp liên kết, hoặc chúng ta phải cảnh báo
cho người sử dụng rằng phép gán chỉ là phép gán có ngữ nghĩa tham chiếu, hoặc
chúng ta phải làm cho trình biên dịch C++ thực hiện phép gán có ngữ nghĩa trị.
Trong C++, chúng ta hiện thực một phương thức đặc biệt gọi là phép gán được
định nghĩa lại (overloaded assignment) để định nghĩa lại cách thực hiện phép
gán. Khi trình biên dịch C++ dịch một phép gán có dạng x = y, nó ưu tiên chọn
phép gán được định nghĩa lại trước nếu như lớp x có định nghĩa. Chỉ khi không có
phương thức này, trình biên dịch mới dịch phép gán như là phép gán từng bit đối
với các thuộc tính của đối tượng (nếu thuộc tính là con trỏ thì phép gán trở thành
phép gán có ngữ nghĩa tham chiếu). Chúng ta cần định nghĩa lại để phép gán cho
ngăn xếp liên kết trở thành phép gán có ngữ nghĩa trị.
Có một vài cách để khai báo và hiện thực phép gán được định nghĩa lại. Cách
đơn giản là khai báo như sau:
void Stack::operator= ( const Stack &original );
Phép gán này có thể được gọi như sau:
x.operator = (y);
hoặc gọi theo cú pháp thường dùng:
x = y;
Phép gán định nghĩa lại cho ngăn xếp liên kết cần làm những việc sau:
• Chép dữ liệu từ ngăn xếp được truyền vào thông qua thông số.
• Giải phóng vùng nhớ chiếm giữ bởi dữ liệu của đối tượng ngăn xếp đang
được thực hiện lệnh gán.
• Chuyển các dữ liệu vừa chép được cho đối tượng ngăn xếp được gán.
template
void Stack::operator = (const Stack &original) // Overload assignment
/*
post: đối tượng ngăn xếp được gán chứa các dữ liệu giống hệt ngăn xếp được truyền vào qua
thông số.
*/
{
Node *new_top, *new_copy, *original_node = original.top_node;
if (original_node == NULL) new_top = NULL;
else { // Tạo bản sao các node.
new_copy = new_top = new Node(original_node->entry);
Chương 2 – Ngăn xếp
Giáo trình Cấu trúc dữ liệu và Giải thuật 33
while (original_node->next != NULL) {
original_node = original_node->next;
new_copy->next = new Node(original_node->entry);
new_copy = new_copy->next;
}
}
while (!empty()) // Làm rỗng ngăn xếp sẽ được gán.
pop();
top_node = new_top; // Ngăn xếp được gán sẽ nắm giữ bản sao.
}
Lưu ý rằng trong phương thức trên chúng ta tạo ra một bản sao từ ngăn xếp
original trước, rồi mới dọn dẹp ngăn xếp sẽ được gán bằng cách gọi phương
thức pop nhiều lần. Nhờ vậy nếu trong ứng dụng có phép gán x = x thì dữ liệu
cũng không bị sai.
Phép gán được định nghĩa lại như trên thỏa yêu cầu là phép gán có ngữ nghĩa
trị, tuy nhiên để có thể sử dụng trong trường hợp:
first_stack=second_stack=third_stack=..., phép gán phải trả về
Stack& (tham chiếu đến lớp Stack).
2.4.3.3. Copy constructor
Trường hợp cuối cùng về sự thiếu an toàn của các cấu trúc liên kết là khi trình
biên dịch cần chép một đối tượng. Chẳng hạn, một đối tượng cần được chép khi
nó là tham trị gởi cho một hàm. Trong C++, tác vụ chép mặc nhiên là chép các
thuộc tính thành phần của lớp. Cũng giống như minh họa trong hình 2.7, tác vụ
chép mặc nhiên này sẽ dẫn đến việc các đối tượng cùng chia xẻ dữ liệu. Nói một
cách khác, tác vụ chép mặc định có ngữ nghĩa tham chiếu khi đối tượng có thuộc
tính kiểu con trỏ. Điều này làm cho người sử dụng có thể vô tình làm mất dữ
liệu:
void destroy_the_stack (Stack copy)
{
}
int main()
{
Stack vital_data;
destroy_the_stack(vital_data);
}
Hàm destroy_the_stack nhận một bản sao copy của vital_data. Bản sao
này cùng chia xẻ dữ liệu với vital_data, do đó khi kết thúc hàm, destructor thực
hiện trên bản sao copy sẽ làm mất luôn dữ liệu của vital_data.
C++ cho phép lớp có thêm phương thức copy constructor để tạo một đối
tượng mới giống một đối tượng đã có. Nếu chúng ta hiện thực copy constructor
Chương 2 – Ngăn xếp
Giáo trình Cấu trúc dữ liệu và Giải thuật 34
cho lớp Stack thì trình biên dịch C++ sẽ ưu tiên chọn tác vụ chép này thay cho
tác vụ chép mặc định. Chúng ta cần hiện thực copy constructor để có được ngữ
nghĩa trị.
Đối với mọi lớp, khai báo chuẩn cho copy constructor cũng giống như khai báo
constructor nhưng có thêm thông số là tham chiếu hằng đến đối tượng của lớp:
Stack::Stack ( const Stack &original );
Do đối tượng gọi copy constructor là một đối tượng rỗng vừa được tạo mới nên
chúng ta không phải lo dọn dẹp dữ liệu như trường hợp đối với phép gán được
định nghĩa lại. Chúng ta chỉ việc chép node đầu tiên và sau đó dùng vòng lặp để
chép tiếp các node còn lại.
template
Stack::Stack(const Stack &original) // copy constructor
/*
post: đối tượng ngăn xếp vừa được tạo ra có dữ liệu giống với ngăn xếp original
*/
{
Node *new_copy, *original_node = original.top_node;
if (original_node == NULL) top_node = NULL;
else { // Tạo bản sao cho các node.
top_node = new_copy = new Node(original_node->entry);
while (original_node->next != NULL) {
original_node = original_node->next;
new_copy->next = new Node(original_node->entry);
new_copy = new_copy->next;
}
a(
}
Một cách tổng quát, đối với mọi lớp liên kết, hoặc chúng ta bổ sung copy
constructor, hoặc chúng ta cảnh báo người sử dụng rằng việc chép đối tượng có
ngữ nghĩa tham chiếu.
2.4.4. Đặc tả ngăn xếp liên kết đã hiệu chỉnh
Chúng ta kết thúc phần này bằng đặc tả đã được hiệu chỉnh dưới đây cho ngăn
xếp liên kết. Phần đặc tả này có được mọi đặc tính an toàn mà chúng ta đã phân
tích.
template
class Stack {
public:
// Các phương thức chuẩn của lớp Stack:
Stack();
bool empty() const;
ErrorCode push(const Entry &item);
ErrorCode pop();
ErrorCode top(Entry &item) const;
Chương 2 – Ngăn xếp
Giáo trình Cấu trúc dữ liệu và Giải thuật 35
// Các đặc tả đảm bảo tính an toàn cho cấu trúc liên kết:
~Stack();
Stack(const Stack &original);
void operator =(const Stack &original);
protected:
Node *top_node;
};
Trên đây là phần trình bày đầy đủ nhất về những yêu cầu cần có đối với ngăn
xếp liên kết, nhưng nó cũng đúng với các cấu trúc liên kết khác. Trong các phần
sau của giáo trình này sẽ không giải thích thêm về 3 tác vụ này nữa, sinh viên tự
phải hoàn chỉnh khi hiện thực bất kỳ CTDL nào có thuộc tính kiểu con trỏ.
Chương 2 – Ngăn xếp
Giáo trình Cấu trúc dữ liệu và Giải thuật 36
File đính kèm:
giao_trinh_cau_truc_du_lieu_va_giai_thuat_chuong_2_ngan_xep.pdf
