QLSV N13

You might also like

Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 58

TRƯỜNG ĐẠI HỌC GIAO THÔNG VẬN TẢI

KHOA CÔNG NGHỆ THÔNG TIN


BỘ MÔN CÔNG NGHỆ THÔNG TIN
------------

BÀI TẬP LỚN

BÁO CÁO ĐỀ TÀI QUẢN LÝ SINH VIÊN

Họ Và Tên : Văn Giang Tài


Lớp : Kỹ Thuật Điện tử-Viễn thông
Khóa : 63
Giáo Viên hướng dẫn : Trần Thị Dung

THÀNH PHỐ HỒ CHÍ MINH – 2023


Mục lục
Phần 1 : Dịch sách
5 Ngăn xếp, Hàng đợi và Deques
5.1 Ngăn xếp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.1 Kiểu dữ liệu trừu tượng ngăn xếp . . . . . . . . . . . . . .
5.1.2 Ngăn xếp STL . . . . . . . . . . . . . . . . . . . . . .
5.1.3 Giao diện ngăn xếp C ++ . . . . . . . . . . . . . . . . . .
5.1.4 Triển khai ngăn xếp dựa trên mảng đơn giản . . . . . .
5.1.5 Triển khai ngăn xếp với danh sách được liên kết chung . . . .
5.1.6 Đảo ngược vectơ bằng cách sử dụng ngăn xếp . . . . . . . . . . . . .
5.1.7 Đối sánh dấu ngoặc đơn và thẻ HTML . . . . . . . . .
5.2 Hàng đợi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.1 Kiểu dữ liệu trừu tượng hàng đợi . . . . . . . . . . . . .
5.2.2 Hàng đợi STL . . . . . . . . . . . . . . . . . . . . . .
5.2.3 Giao diện hàng đợi C ++ . . . . . . . . . . . . . . . . . .
5.2.4 Triển khai dựa trên mảng đơn giản . . . . . . . . . .
5.2.5 Thực hiện hàng đợi với danh sách liên kết vòng tròn . . .
5.3 Hàng đợi hai đầu . . . . . . . . . . . . . . . . . . . . . .
5.3.1 Kiểu dữ liệu trừu tượng Deque . . . . . . . . . . . . .
5.3.2 The STL Deque . . . . . . . . . . . . . . . . . . . . . .
5.3.3 Triển khai Deque với danh sách được liên kết gấp đôi . . . .
5.3.4 Bộ điều hợp và mẫu thiết kế bộ điều hợp . . . . . . . .
Cây
7.1 Cây tổng hợp . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.1.1 Định nghĩa và tính chất của cây . . . . . . . . . . . . . .
7.1.2 Chức năng cây . . . . . . . . . . . . . . . . . . . . . . .
7.1.3 Giao diện cây C ++ . . . . . . . . . . . . . . . . . . .
7.1.4 Cấu trúc liên kết cho cây thông thường. . . . . . . . . . .
7.2 Thuật toán truyền tải cây. . . . . . . . . . . . . . . . . . .
7.2.1 Chiều sâu và chiều cao . . . . . . . . . . . . . . . . . . . . .
7.2.3 Truyền tải thứ tự sau. . . . . . . . . . . . . . . . . . . .
7.3 Cây nhị phân . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.3.1 Cây nhị phân ADT . . . . . . . . . . . . . . . . . . .
7.3.2 Giao diện cây nhị phân C ++ . . . . . . . . . . . . . . .
7.3.3 Tính chất của cây nhị phân . . . . . . . . . . . . . . . .
7.3.4 Một cấu trúc liên kết cho cây nhị phân . . . . . . . . . . .
7.3.5 Cấu trúc dựa trên vector cho cây nhị phân . . . . . . . .
7.3.6 Truyền cây nhị phân. . . . . . . . . . . . . . . .
7.3.7 Mẫu Hàm Mẫu . . . . . . . . . . . . .
Phần 2 : BÁO CÁO ĐỀ TÀI QUẢN LÝ SINH VIÊN
Phần 1 : Dịch sách
Chương 5:Ngăn xếp, Hàng đợi và Deques
5.1 Ngăn xếp
Một ngăn xếp chứa các đối tượng được chèn và loại bỏ theo cuối cùng-theo nguyên tắc
xuất trước (LIFO). Các đối tượng có thể được chèn vào một ngăn xếp bất cứ lúc nào,
nhưng chỉ đối tượng được chèn gần đây nhất (nghĩa là "cuối cùng") mới có thể bị xóa bất
cứ lúc nào. Trong trường hợp này, các hoạt động cơ bản liên quan đến "đẩy" và "bật" của
các tấm trên ngăn xếp. Khi chúng ta cần một tấm mới từ bộ phân phối, chúng tôi "bật"
tấm trên cùng ra khỏi ngăn xếp và khi chúng tôi thêm một tấm,chúng tôi "đẩy" nó xuống
ngăn xếp để trở thành tấm trên cùng mới.(Xem Hình 5.1.) Ngăn xếp là một dữ liệu cơ bản
của cấu trúc. Chúng được sử dụng trong nhiều ứng dụng, bao gồm những điều sau đây.
Ví dụ 5.1: Trình duyệt web Internet lưu trữ địa chỉ của các trang web được truy cập gần
đây trên một ngăn xếp.

Hình 5.1: Bản vẽ sơ đồ của bộ phân phối; một triển khai vật lý của ngăn xếp ADT.
5.1.1 Kiểu dữ liệu trừu tượng ngăn xếp
Ngăn xếp là cấu trúc đơn giản nhất trong tất cả các cấu trúc dữ liệu, nhưng chúng cũng là
một trong những cấu trúc dữ liệu nhất quan trọng, vì chúng được sử dụng trong một loạt
các ứng dụng khác nhau bao gồm nhiều cấu trúc dữ liệu phức tạp hơn. Chính thức, ngăn
xếp là một kiểu dữ liệu trừu tượng (ADT) hỗ trợ các hoạt động sau:
 push(e): Chèn phần tử e ở đầu ngăn xếp.
 pop():Loại bỏ phần tử trên cùng khỏi ngăn xếp; Đã xảy ra lỗi nếu ngăn xếp trống.
 top():Trả về tham chiếu đến phần tử trên cùng trên ngăn xếp, với- ra loại bỏ nó;
Lỗi xảy ra nếu ngăn xếp trống.
Ngoài ra, chúng ta cũng hãy định nghĩa các hàm hỗ trợ sau:
 size():Trả về số phần tử trong ngăn xếp.
 empty():Trả về true nếu ngăn xếp trống và false nếu không.

5.1.2 Ngăn xếp STL


Thư viện mẫu chuẩn cung cấp một triển khai của một ngăn xếp.Việc triển khai derlying
dựa trên lớp vectơ STL, được trình bày trong Mục 1.5.5 và 6.1.4. Để khai báo một đối
tượng của ngăn xếp kiểu, nó là cần thiết trước tiên bao gồm tệp định nghĩa, được gọi là
"ngăn xếp".Như với STL vector, ngăn xếp lớp là một phần của không gian tên STD, vì
vậy cần phải sử dụng "std::stack" hoặc để cung cấp câu lệnh "using".Ví dụ: đoạn mã bên
dưới khai báo một chồng số nguyên.
#include <stack>
using std::stack;
stack<int> myStack;
Chúng tôi gọi loại phần tử riêng lẻ là loại cơ sở của ngăn xếp. Như với STL vector, một
ngăn xếp STL tự động thay đổi kích thước khi các phần tử mới được đẩy lên. Dưới đây,
chúng tôi Liệt kê các chức năng chính.
 size(): Trả về số phần tử trong stack.
 empty(): Trả về true nếu ngăn xếp rỗng và false nếu không.
 push(e): Đẩy e lên trên cùng của ngăn xếp.
 pop(): Bật phần tử ở đầu ngăn xếp.
 top(): Trả về một tham chiếu đến phần tử ở đầu ngăn xếp.
5.1.3 Giao diện ngăn xếp C ++
Khi xác định kiểu dữ liệu trừu tượng, mối quan tâm chính của chúng tôi là chỉ định Giao
diện lập trình ứng dụng (API), hoặc đơn giản là giao diện, trong đó mô tả tên của các
thành viên công cộng mà ADT phải hỗ trợ và cách chúng được khai báo và sử dụng .
Ngôn ngữ lập trình C++ không cung cấp một phương pháp đơn giản để xác định giao
diện, và do đó, giao diện được xác định ở đây không phải là chính thức Lớp C++.Giao
diện không chính thức cho ngăn xếp ADT được đưa ra trong đoạn code 5.1.Trong đoạn
code 5.1, loại phần tử này được biểu thị bằng E.
template <typename E>
class Stack {
public:
int size() const;
bool empty() const;
const E& top() const throw(StackEmpty);
void push(const E& e);
void pop() throw(StackEmpty);
};
Đoạn code 5.1: Một giao diện Stack không chính thức (không phải là một lớp C ++ hoàn
chỉnh). Trên cùng đều được khai báo là const, thông báo cho trình biên dịch rằng chúng
không làm thay đổi nội dung của ngăn xếp. Đầu hàm member trả về một tham chiếu
không đổi đến đầu ngăn xếp. Hàm push thành viên lấy một tham chiếu liên tục đến một
đối tượng loại E. Tình trạng lỗi xảy ra khi gọi một trong hai hàm bật lên hoặc trên cùng
trên một ngăn xếp trống. Điều này được báo hiệu bằng cách ném một ngoại lệ của loại
StackEmpty, mà được định nghĩa trong Code đoạn 5.2.
class StackEmpty : public RuntimeException {
public:
StackEmpty(const string& err) : RuntimeException(err) {}
};
Đoạn code 5.2: Ngoại lệ bởi các hàm pop và top khi được gọi trên một ngăn xếp trống.
Lớp này có nguồn gốc từ RuntimeException từ Phần 2.4.
5.1.4 Triển khai ngăn xếp dựa trên mảng đơn giản
Chúng ta có thể thực hiện một ngăn xếp bằng cách lưu trữ các phần tử của nó trong một
mảng. (Xem Hình 5.2.)

Hình 5.2: Hiện thực hóa một ngăn xếp bằng một mảng S. Yếu tố hàng đầu trong ngăn xếp
được lưu trữ trong ô S [t].
Chúng tôi cũng giới thiệu một cái mới, được gọi là StackFull, để báo hiệu tình trạng lỗi
phát sinh nếu chúng ta cố gắng chèn một phần tử mới và mảng S đã đầy. Ngoại lệ
StackFull dành riêng cho việc triển khai ngăn xếp của chúng tôi và không được xác định
trong ngăn xếp ADT. Với điều này mới ngoại lệ, sau đó chúng ta có thể triển khai các
hàm ADT ngăn xếp như được mô tả trong đoạn code 5.3.
Algorithm size():
return t +1
Algorithm empty():
return (t < 0)
Algorithm top():
if empty() then
throw StackEmpty exception
return S[t]
Algorithm push(e):
if size() = N then
throw StackFull exception
t ← t +1
S[t] ← e
Algorithm pop():
if empty() then
throw StackEmpty exception
t ← t −1
Đoạn code 5.3: Triển khai một stack bằng một mảng.
Bảng 5.1 cho thấy thời gian chạy cho các hàm thành viên trong việc thực hiện ngăn xếp
theo mảng.

Bảng 5.1: Hiệu suất của ngăn xếp dựa trên mảng
Triển khai C++ của Stack
Trong phần này, chúng tôi trình bày một triển khai C ++ cụ thể của giả ở trên- đặc tả mã
bằng một lớp, được gọi là ArrayStack.Chúng tôi để việc triển khai của họ như một bài
tập. Chúng ta bắt đầu bằng cách cung cấp định nghĩa lớp ArrayStack trong đoạn code 5.4.
template <typename E>
class ArrayStack {
enum { DEF CAPACITY = 100 };
public:
ArrayStack(int cap = DEF CAPACITY);
int size() const;
bool empty() const;
const E& top() const throw(StackEmpty);
void push(const E& e) throw(StackFull);
void pop() throw(StackEmpty);
private:
E* S;
int capacity;
int t;
};
Đoạn code 5.4: Lớp ArrayStack, triển khai giao diện Stack.
Chúng tôi sử dụng một bảng liệt kê để xác định giá trị dung lượng mặc định này. Lớp của
chúng ta được tạo khuôn mẫu với kiểu phần tử, ký hiệu là E. Bộ nhớ của ngăn xếp, ký
hiệu là S, là một mảng được phân bổ động của loại E, nghĩa là một con trỏ đến E. Tiếp
theo, chúng tôi trình bày việc triển khai các hàm thành viên ArrayStack trong Đoạn code
5.5.
template <typename E> ArrayStack<E>::ArrayStack(int cap)
: S(new E[cap]), capacity(cap), t(−1) { }
template <typename E> int ArrayStack<E>::size() const
{ return (t + 1); }
template <typename E> bool ArrayStack<E>::empty() const
{ return (t < 0); }
template <typename E>
const E& ArrayStack<E>::top() const throw(StackEmpty) {
if (empty()) throw StackEmpty("Top of empty stack");
return S[t];
}
template <typename E>
void ArrayStack<E>::push(const E& e) throw(StackFull) {
if (size() == capacity) throw StackFull("Push to full stack");
S[++t] = e;
}
template <typename E>
void ArrayStack<E>::pop() throw(StackEmpty) {
if (empty()) throw StackEmpty("Pop from empty stack");
−−t;
}
Đoạn code 5.5: Triển khai các hàm thành viên của class ArrayStack.
Trong đoạn code 5.6 dưới đây, chúng tôi trình bày một ví dụ về việc sử dụng ArrayStack
của chúng tôi lớp.Phiên bản A là một chồng các số nguyên có dung lượng mặc định
(100). Trường hợp B là một chồng chuỗi ký tự có dung lượng 10.
ArrayStack<int> A; // A = [ ], size = 0
A.push(7); // A = [7*], size = 1
A.push(13); // A = [7, 13*], size = 2
cout << A.top() << endl; A.pop(); // A = [7*], outputs: 13
A.push(9); // A = [7, 9*], size = 2
cout << A.top() << endl; // A = [7, 9*], outputs: 9
cout << A.top() << endl; A.pop(); // A = [7*], outputs: 9
ArrayStack<string> B(10); // B = [ ], size = 0
B.push("Bob"); // B = [Bob*], size = 1
B.push("Alice"); // B = [Bob, Alice*], size = 2
cout << B.top() << endl; B.pop(); // B = [Bob*], outputs: Alice
B.push("Eve"); // B = [Bob, Eve*], size = 2
Đoạn code 5.6: Một ví dụ về việc sử dụng class ArrayStack. Nội dung của ngăn xếp được
hiển thị trong nhận xét sau thao tác. Đầu ngăn xếp được biểu thị bằng dấu hoa thị ("*").
5.1.5 Triển khai Stack với Danh sách Liên kết Chung
Trong phần này, chúng tôi chỉ ra cách triển khai ngăn xếp ADT bằng cách sử dụng danh
sách được liên kết đơn lẻ.Trong ví dụ này, chúng ta định nghĩa Elem thuộc loại chuỗi.
Chúng tôi để lại nhiệm vụ tạo ra một triển khai thực sự chung chung như một bài tập.
(Xem Bài tập R-5.7.)
typedef string Elem;
class LinkedStack {
public:
LinkedStack();
int size() const;
bool empty() const;
const Elem& top() const throw(StackEmpty);
void push(const Elem& e);
void pop() throw(StackEmpty);
private: // member data
SLinkedList<Elem> S;
int n;
};
Đoạn code 5.7: Lớp LinkedStack, một triển khai danh sách được liên kết của một ngăn
xếp.
Trong đoạn code 5.8, chúng tôi trình bày các triển khai của hàm xây dựng và kích thước
và các chức năng trống. Trình tạo của chúng ta tạo ngăn xếp ban đầu và ban đầu- N về
không. Chúng tôi không cung cấp trình hủy rõ ràng, thay vào đó dựa vào SLinkedList
destructor để xử lý danh sách được liên kết S.
LinkedStack::LinkedStack()
: S(), n(0) { }
int LinkedStack::size() const
{ return n; }
bool LinkedStack::empty() const
{ return n == 0; }
Đoạn Code 5.8: Các hàm xây dựng và kích thước cho lớp LinkedStack.
Các định nghĩa về các hoạt động ngăn xếp, trên cùng, đẩy và pop, được trình bày trong
đoạn code 5.9. Vì SLinkedList chỉ có thể chèn và xóa các phần tử trong thời gian không
đổi ở đầu, đầu rõ ràng là sự lựa chọn tốt hơn. Do đó, chức năng thành viên trả về trên
cùng S.front(). Các hàm push và pop gọi các hàm addFront và removeFront, tương ứng,
và cập nhật số lượng phần tử.
const Elem& LinkedStack::top() const throw(StackEmpty) {
if (empty()) throw StackEmpty("Top of empty stack");
return S.front();
}
void LinkedStack::push(const Elem& e) {
++n;
S.addFront(e);
}
void LinkedStack::pop() throw(StackEmpty) {
if (empty()) throw StackEmpty("Pop from empty stack");
−−n;
S.removeFront();
}
Đoạn code 5.9: Các hoạt động chính cho lớp LinkedStack.
5.1.6 Đảo ngược vectơ bằng cách sử dụng ngăn xếp
Chúng ta có thể sử dụng một ngăn xếp để đảo ngược các phần tử trong một vector, do đó
tạo ra một thuật toán chữ thảo cho bài toán đảo ngược mảng được giới thiệu trong Phần
3.5.1. Các Ý tưởng cơ bản là đẩy tất cả các phần tử của vectơ theo thứ tự vào một ngăn
xếp và sau đó Tô lại vectơ bằng cách bật các phần tử ra khỏi ngăn xếp. Trong mã Phân
đoạn 5.10, chúng tôi đưa ra một triển khai C ++ của thuật toán này.

template <typename E>


void reverse(vector<E>& V) {
ArrayStack<E> S(V.size());
for (int i = 0; i < V.size(); i++)
S.push(V[i]);
for (int i = 0; i < V.size(); i++) {
V[i] = S.top(); S.pop();
}
}
Đoạn Code 5.10: Một hàm chung sử dụng ngăn xếp để đảo ngược vector.
5.1.7 Dấu ngoặc đơn và thẻ HTML phù hợp
Trong phần này, chúng ta khám phá hai ứng dụng liên quan của ngăn xếp. Đầu tiên là kết
hợp dấu ngoặc đơn và nhóm các ký hiệu trong biểu thức số học. Số học expres- Sion có
thể chứa các cặp ký hiệu nhóm khác nhau, chẳng hạn như
• Parentheses: “(” and “)”
• Braces: “{” and “}”
• Brackets: “[” and “]”
• Floor function symbols: “⌊” and “⌋”
• Ceiling function symbols: “⌈” and “⌉,”
Và mỗi biểu tượng mở đầu phải khớp với biểu tượng đóng tương ứng của nó. Cho Ví dụ:
ký hiệu dấu ngoặc trái ("[") phải khớp với dấu ngoặc phải tương ứng ("]") như trong biểu
thức sau:
• Correct: ( )(( )){([( )])}
• Correct: ((( )(( )){([( )])}))
• Incorrect: )(( )){([( )])}
• Incorrect: ({[])}
• Incorrect: (
Một thuật toán để khớp dấu ngoặc đơn
Một vấn đề quan trọng trong việc xử lý các biểu thức số học là đảm bảo nhóm các biểu
tượng chính xác.Chúng ta có thể sử dụng ngăn xếp S để thực hiện kết hợp nhóm các ký
hiệu trong một biểu thức số học với một lần quét từ trái sang phải. Thuật toán kiểm tra
rằng các biểu tượng trái và phải khớp với nhau và cả bên trái và bên phải đều cùng loại.
Giả sử chúng ta được cho một dãy X = x0x1x2 ... xn−1. Mỗi lần chúng ta bắt gặp một
biểu tượng mở đầu, chúng ta đẩy biểu tượng đó lên S và mỗi khi chúng ta gặp một biểu
tượng đóng, chúng ta bật biểu tượng biểu tượng trên cùng từ ngăn xếp S (giả sử S không
trống) và chúng tôi kiểm tra xem hai biểu tượng này có trống không Biểu tượng có các
loại tương ứng.Chúng tôi cung cấp một mô tả mã giả về thuật toán này trong đoạn code
5.11.
Thuật toán ParenMatch(X,n):
Input: Một mảng X gồm n token, mỗi token là một ký hiệu nhóm, a
biến, toán tử số học hoặc số
Output: true nếu và chỉ khi tất cả các ký hiệu nhóm trong X khớp
Hãy để S là một ngăn xếp trống
for i ← 0 to n−1 do
if X[i] là một biểu tượng nhóm mở đầu sau đó
S.push(X[i])
else if X[i] là một biểu tượng nhóm đóng sau đó
if S.empty() then
return false {không có gì để phù hợp}
if S.top() không khớp với loại X[i] thì
trả về FALSE {wrong type}
S.pop()
if S.empty() then
return true {mọi ký hiệu đều khớp}
else
return false {Một số ký hiệu không bao giờ khớp}
Đoạn code 5.11: Thuật toán kết hợp các ký hiệu nhóm trong số học
biểu thức.
Thẻ phù hợp trong tài liệu HTML
Một ứng dụng khác trong đó kết hợp là quan trọng là trong việc xác thực HTML Tài liệu.
Trong tài liệu HTML, các phần của văn bản được phân tách bằng thẻ HTML. Một thẻ HTML mở đơn
giản có dạng "<name>" và đóng tương ứng có dạng "</name>." Các thẻ HTML thường được sử
dụng bao gồm:
• body: Nội dung tài liệu
• h1: Tiêu đề phần
• center: trung tâm phần
• p: đoạn
• ol: Danh sách đánh số (theo thứ tự)
• li: Mục danh sách
Chúng tôi hiển thị một tài liệu HTML mẫu và khả năng kết xuất trong Hình 5.3. Mục tiêu
là viết một chương trình để kiểm tra xem các thẻ có khớp đúng không.cách tiếp cận rất
giống trong đoạn code 5.11 có thể sử dụng dể khớp với các thẻ trong tài liệu HTML.
Chúng tôi đẩy từng thẻ mở vào một ngăn xếp và khi chúng tôi gặp thẻ đóng, chúng tôi sẽ
bật ngăn xếp và xác minh
<body>
<center>
<h1> Chiếc thuyền nhỏ </h1>
</center>
<p> Cơn bão quăng quật đứa nhỏ
Thuyền như một đôi giày thể thao rẻ tiền trong một
máy giặt cũ. Ba ngư dân say xỉn đã quen với
Điều trị như vậy, tất nhiên, nhưng
Không phải người bán cây, người thậm chí còn
Như một người xếp hàng bây giờ cảm thấy rằng anh ta
đã trả quá nhiều tiền cho chuyến đi. </p>
<ol>
<li> Nhân viên bán hàng sẽ chết? </li>
<li> Thuyền có màu gì? </li>
<li> Còn Naomi thì sao? </li>
</ol>
</body>
Hình 5.3: Thẻ HTML : một tài liệu HTML;
Trong đoạn code 5.12 đến 5.14, chúng tôi trình bày một chương trình C ++ để kết hợp
trong tài liệu HTML được đọc từ luồng đầu vào tiêu chuẩn .Chúng tôi giả định rằng tất cả
các thẻ được hình thành cú pháp tốt. Đầu tiên, thủ tục getHtmlTags đọc từng dòng đầu
vào, trích xuất tất cả dưới dạng chuỗi và lưu trữ chúng trong một vectơ mà nó trả về.
vector<string> getHtmlTags() { // Lưu trữ thẻ trong một vector
vector<string> tags; // Vector của thẻ HTML
while (cin) { // Đọc cho đến khi kết thúc tệp
string line;
getline(cin, line); // Nhập một dòng văn bản đầy đủ
int pos = 0; // Vị trí quét hiện tại
int ts = line.find("<", pos); // có thể bắt đầu thẻ
while (ts != string::npos) { // Lặp lại cho đến khi kết thúc chuỗi
int te = line.find(">", ts+1); // Quét để kết thúc thẻ
tags.push back(line.substr(ts, te−ts+1)); // Chắp thêm thẻ vào vectơ
pos = te + 1; // thêm 1 vị trí
ts = line.find("<", pos);
}
}
return tags; // Trả về vector của thẻ
}
Đoạn code 5.12: Lấy một vector các thẻ HTML từ đầu vào và lưu trữ chúng trong một
vectơ chuỗi.
Với ví dụ thể hiện trong Hình 5.3, thủ tục này sẽ trả về
Vector sau:
<body>, <center>, <h1>, </h1>, </center>, . . . , </body>
Trong đoạn code 5.12, chúng ta sử dụng một biến pos, duy trì vị trí trong dòng đầu.
Chúng tôi sử dụng hàm chuỗi tích hợp tìm để định vị Sự xuất hiện đầu tiên của "<" theo
sau vị trí hiện tại.Thủ tục isHtmlMatched, được hiển thị trong đoạn code 5.12,Quá trình
khớp các thẻ.

// Kiểm tra các thẻ phù hợp


bool isHtmlMatched(const vector<string>& tags) {
LinkedStack S; // ngăn xếp để mở thẻ
typedef vector<string>::const iterator Iter;// Loại bộ lặp
// Lặp qua vectơ
for (Iter p = tags.begin(); p != tags.end(); ++p) {
if (p−>at(1) != ’/’) // thẻ mở?
S.push(*p); // Đẩy nó lên ngăn xếp
else { // đóng các thể khác
if (S.empty()) return false; // không có gì để match - failure
string open = S.top().substr(1); // thẻ mở không bao gồm ’<’
string close = p−>substr(2); // Thẻ đóng không bao gồm ’</’
if (open.compare(close) != 0) return false; // không khớp
else S.pop(); // Yếu tố kết hợp pop
}
}
if (S.empty()) return true; // Mọi thứ đều phù hợp - tốt
else return false; // một số không khớp-xấu
}
Đoạn code 5.13: Kiểm tra xem các thẻ HTML được lưu trữ trong các thẻ vector có phải là
không kết hợp.
Chương trình chính được trình bày trong đoạn code 5.14. Nó gọi các hàm getHtmlTags
để đọc các thẻ, và sau đó nó chuyển chúng sang isHtmlMatched để kiểm tra chúng.
int main() { // trình kiểm tra HTML chính
if (isHtmlMatched(getHtmlTags())) // Nhận thẻ và kiểm tra chúng
cout << "Tệp đầu vào là tài liệu HTML phù hợp." << endl;
else
cout << "Tệp đầu vào không phải là tài liệu HTML phù hợp." << endl;
}
Đoạn code 5.14: Chương trình chính để kiểm tra xem tệp đầu vào có bao gồm
phù hợp với thẻ HTML.
5.2 Hàng đợi
Một cấu trúc dữ liệu cơ bản khác là hàng đợi, là họ hàng gần của Ngăn xếp. Chúng tôi
thường nói rằng các yếu tố đi vào hàng đợi ở phía sau và bị xóa từ phía trước.
5.2.1 Kiểu dữ liệu trừu tượng hàng đợi
Chính thức, kiểu dữ liệu trừu tượng hàng đợi xác định một container giữ các phần tử
trong Một chuỗi, trong đó truy cập và xóa phần tử bị hạn chế ở phần tử đầu tiên trong
chuỗi, được gọi là mặt trước của hàng đợi và chèn phần tử là Giới hạn ở cuối chuỗi, được
gọi là phía sau của hàng đợi. Này Hạn chế thực thi quy tắc rằng các mục được chèn và
xóa trong hàng đợi theo theo nguyên tắc nhập trước xuất trước (FIFO).
Kiểu dữ liệu trừu tượng hàng đợi (ADT) hỗ trợ các thao tác sau:
 enqueue(e): Chèn phần tử e vào phía sau hàng đợi.
 dequeue(): Loại bỏ phần tử ở phía trước của hàng đợi; một lỗi xảy ra nếu hàng đợi
trống.
 front(): Trả lại, nhưng không xóa, một tham chiếu đến ele-ment phía trước trong
hàng đợi; Lỗi xảy ra nếu hàng đợi trống.
Hàng đợi ADT cũng bao gồm các chức năng thành viên hỗ trợ sau:
 size(): Trả về số phần tử trong hàng đợi.
 empty(): Trả về true nếu hàng đợi trống và false nếu không.
Chúng tôi minh họa các thao tác trong ADT hàng đợi trong ví dụ sau.
Ví dụ 5.4: Bảng sau đây hiển thị một loạt các thao tác hàng đợi và chúng
ảnh hưởng đến một hàng đợi trống ban đầu, Q, của các số nguyên.
5.2.2 Hàng đợi STL
Thư viện Mẫu Tiêu chuẩn cung cấp việc triển khai hàng đợi. Như với ngăn xếp STL,
triển khai cơ bản dựa trên lớp vectơ STL (Mục 1.5.5 và 6.1.4). Để khai báo một đối tượng
của hàng đợi kiểu, nó là cần thiết-trước tiên, sary bao gồm tệp định nghĩa, được gọi là
"hàng đợi". Như với STL vector, hàng đợi lớp là một phần của không gian tên STD, vì
vậy cần phải sử dụng "std::queue" hoặc để cung cấp một câu lệnh "sử dụng" thích hợp.
Lớp xếp hàng là được tạo khuôn mẫu với kiểu cơ sở của các phần tử riêng lẻ.
#include <queue>
using std::queue; // Làm cho hàng đợi có thể truy cập được
queue<float> myQueue; // một hàng của float
Như với các trường hợp của vectơ và ngăn xếp STL, hàng đợi STL tự động thay đổi kích
thước chính nó như các yếu tố mới được thêm vào. Dưới đây, chúng tôi liệt kê một số
chức năng chính. Cho q được khai báo là một hàng đợi STL và để e biểu thị một đối
tượng duy nhất có loại giống với loại cơ sở của hàng đợi. (Ví dụ: q là một hàng đợi của
float, và e là float.)
 size(): Trả về số phần tử trong hàng đợi.
 empty(): Trả về true nếu hàng đợi trống và false nếu không.
 push(e): Hàng đợi e ở phía sau hàng đợi.
 pop(): Xếp hàng phần tử ở phía trước hàng đợi.
 front(): Trả về tham chiếu đến phần tử ở phía trước hàng đợi.
 back(): Trả về tham chiếu đến phần tử ở phía sau của hàng đợi.
Không giống như giao diện hàng đợi của chúng tôi, hàng đợi STL cung cấp quyền truy
cập vào cả mặt trước và phía sau hàng đợi. Tương tự như ngăn xếp STL, kết quả của việc
áp dụng bất kỳ các hoạt động trước, sau hoặc bật lên một hàng đợi STL trống là không
xác định.Tùy thuộc vào lập trình viên để đảm bảo rằng không có truy cập bất hợp pháp
nào.
Giao diện của chúng tôi cho hàng đợi ADT được đưa ra trong đoạn code 5.15. Như với
ngăn xếp ADT, lớp được tạo mẫu. Phần tử cơ sở của hàng đợi loại E được cung cấp bởi
người dùng.
template <typename E>
class Queue { // Giao diện cho hàng đợi
public:
int size() const; // Số lượng mục trong hàng đợi
bool empty() const; // Hàng đợi có trống không?
const E& front() const throw(QueueEmpty); // Yếu tố phía trước
void enqueue (const E& e); // phần tử hàng đợi ở phía sau
void dequeue() throw(QueueEmpty); // Phần tử hàng đợi ở phía trước
};
Đoạn code 5.15: Một giao diện hàng đợi không chính thức (không phải là một lớp C ++
hoàn chỉnh).
Lưu ý rằng kích thước và các hàm rỗng có cùng ý nghĩa với coun-terparts trong Stack
ADT. Hai chức năng này được gọi là hàm accessor, vì chúng trả về một giá trị và không
thay đổi nội dung của cấu trúc dữ liệu. Cũng lưu ý việc sử dụng ngoại lệ QueueEmpty để
chỉ ra lỗi trạng thái của một hàng đợi trống.Kích thước hàm thành viên, trống và mặt
trước đều được khai báo là const,thông báo cho trình biên dịch rằng chúng không làm
thay đổi nội dung của hàng đợi. Ghi rằng phía trước hàm member trả về một tham chiếu
không đổi đến đầu hàng đợi.Tình trạng lỗi xảy ra khi gọi một trong hai hàm ở phía trước
hoặc hàng đợi trên một hàng đợi trống. Điều này được báo hiệu bằng cách ném một ngoại
lệ QueueEmpty, mà được định nghĩa trong đoạn code 5.16.
class QueueEmpty : public RuntimeException {
public:
QueueEmpty(const string& err) : RuntimeException(err) { }
};
Đoạn code 5.16: Ngoại lệ được ném bởi các hàm ở phía trước hoặc hàng đợi khi được gọi
trên một hàng đợi trống. Lớp này có nguồn gốc từ RuntimeException từ Phần 2.4.
5.2.4 Triển khai dựa trên mảng đơn giản
Chúng tôi trình bày một nhận thức về hàng đợi bằng một mảng, Q, với dung lượng N, để
lưu trữ các yếu tố của nó. Vấn đề chính với việc thực hiện này là quyết định Làm thế nào
để theo dõi phía trước và phía sau của hàng đợi. Một là điều chỉnh cách tiếp cận mà
chúng tôi đã sử dụng để triển khai ngăn xếp. Cụ thể, hãy để Q[0] là phía trước của hàng
đợi và để hàng đợi phát triển. Tuy nhiên, đây không phải là một giải pháp hiệu quả vì nó
đòi hỏi chúng ta phải di chuyển tất cả các phần tử chuyển tiếp một ô mảng mỗi khi chúng
ta thực hiện thao tác dequeue. Do đó, việc triển khai như vậy sẽ cần thời gian Θ (n) để
thực hiện hàng đợi hàm, trong đó n là số phần tử hiện tại trong hàng đợi. Nếu chúng ta
muốn đạt được thời gian không đổi cho mỗi chức năng hàng đợi, chúng ta cần một cách
tiếp cận khác nhau.
Sử dụng một mảng theo cách vòng tròn
Để tránh các đối tượng chuyển động khi chúng được đặt trong Q, chúng ta định nghĩa ba
biến, f, r,n, có nghĩa như sau:
• f là chỉ số của ô Q lưu trữ mặt trước của hàng đợi. Nếu hàng đợi là nonempty, đây là chỉ
mục của phần tử sẽ được loại bỏ bởi dequeue.
• r là chỉ số của ô Q theo sau hàng đợi. Nếu hàng đợi là không đầy đủ, đây là chỉ mục nơi
phần tử được chèn bởi enqueue.
• n là số phần tử hiện tại trong hàng đợi.
Ban đầu, chúng tôi đặt n = 0 và f = r = 0, cho biết một hàng đợi trống. Khi chúng tôi
Dequeue một phần tử từ phía trước của hàng đợi, chúng ta giảm n và tăng f
đến ô tiếp theo trong Q. Tương tự như vậy, khi chúng ta enqueue một phần tử, chúng ta
tăng r và gia tăng n. Điều này cho phép chúng ta triển khai các hàm enqueue và dequeue
trong thời gian không đổi
Tuy nhiên, vẫn còn một vấn đề với cách tiếp cận này. Hãy xem xét, ví dụ,
điều gì xảy ra nếu chúng ta liên tục enqueue và dequeue một phần tử N khác nhau. Chúng
ta sẽ có f = r = N. Nếu sau đó chúng ta cố gắng chèn phần tử
Chỉ một lần nữa, chúng ta sẽ nhận được một lỗi mảng ngoài giới hạn, mặc dù ở đó
là rất nhiều chỗ trong hàng đợi trong trường hợp này. Để tránh vấn đề này và có thể:
Sử dụng tất cả các mảng Q, chúng ta để các chỉ số f và r "quấn quanh" phần cuối của Q.
Nghĩa là, bây giờ chúng ta xem Q là một "mảng tròn" đi từ Q[0] đến Q[N − 1] và
sau đó ngay lập tức quay lại Q[0] một lần nữa. (Xem Hình 5.4.)

Hình 5.4: Sử dụng mảng Q theo kiểu tròn: (a) cấu hình "bình thường" với
f ≤ r; (b) cấu hình "quấn quanh" với r < f. Các ô lưu trữ hàng đợi
các yếu tố được tô bóng.
Sử dụng toán tử modulo để thực hiện một mảng tròn
Thực hiện quan điểm vòng tròn này của Q thực sự khá dễ dàng. Mỗi lần chúng tôi vào-
tạo f hoặc r, chúng ta chỉ cần tính gia số này là "(f + 1) mod N" hoặc "(r + 1) mod N",
tương ứng, trong đó toán tử "mod" là toán hạng modulo oper-ator." Toán tử này được
tính cho một số dương bằng cách lấy phần còn lại sau khi phân chia tích phân. Ví dụ, 48
chia cho 5 là 9 với phần dư 3, Vì vậy, 48 mod 5 = 3. Cụ thể, cho trước các số nguyên x và
y, sao cho x ≥ 0 và y > 0, x mod y là số nguyên duy nhất 0 ≤ r < y sao cho x = qy + r, đối
với một số nguyên q. Hãy nhớ lại rằng C++ sử dụng "%" để biểu thị toán tử modulo.
Chúng tôi trình bày việc triển khai của chúng tôi trong đoạn code 5.17. Lưu ý rằng chúng
tôi có giới thiệu một loại mới, được gọi là QueueFull, để báo hiệu rằng không còn phần
tử nào có thể được chèn vào hàng đợi. Việc triển khai hàng đợi của chúng tôi bằng một
mảng là tương tự như của một ngăn xếp, và được để lại như một bài tập.
Algorithm size():
return n
Algorithm empty():
return (n = 0)
Algorithm front():
if empty() then
throw QueueEmpty exception
return Q[ f ]
Algorithm dequeue():
if empty() then
throw QueueEmpty exception
f ← (f +1) mod N
n = n−1
Algorithm enqueue(e):
if size() = N then
throw QueueFull exception
Q[r] ← e
r ← (r +1) mod N
n = n+1
Đoạn code 5.17: Thực hiện một hàng đợi bằng cách sử dụng một mảng tròn.
5.2.5 Thực hiện hàng đợi với danh sách liên kết vòng tròn
Trong phần này, chúng tôi trình bày triển khai C ++ của ADT hàng đợi bằng cách sử
dụng một liên kết sự đại diện. Hãy nhớ lại rằng chúng tôi xóa khỏi đầu hàng đợi và chèn
vào sau. Do đó, không giống như ngăn xếp được liên kết trong đoạn code 5.7, chúng tôi
không thể sử dụng đơn lẻ lớp danh sách được liên kết, vì nó chỉ cung cấp quyền truy cập
hiệu quả vào một bên của danh sách.Thay vào đó cách tiếp cận của chúng tôi là sử dụng
danh sách được liên kết tròn, được gọi là CircleList, đó là
được giới thiệu trước đó trong Mục 3.4.1.
Hãy nhớ lại rằng CircleList duy trì một con trỏ, được gọi là cursor,những điểm
của danh sách. Cũng nhớ lại rằng CircleList cung cấp hai chức năng thành viên,phía sau
và phía trước. Hàm trở lại trả về một tham chiếu đến phần tử mà con trỏ đến
điểm và mặt trước hàm trả về một tham chiếu đến phần tử ngay lập tức theo sau nó trong
danh sách tròn. Để thực hiện một hàng đợi, phần tử được tham chiếu
Phía sau sẽ là phía sau của hàng đợi và phần tử được tham chiếu bởi phía trước sẽ là
mặt trận.Cũng nhớ lại rằng CircleList hỗ trợ các chức năng sửa đổi sau. Các chức năng
thêm chèn một nút mới ngay sau con trỏ, hàm loại bỏ nút ngay sau con trỏ và chức năng
tiến bộ di chuyển con trỏ chuyển tiếp đến nút tiếp theo của danh sách tròn. Để thực hiện
enqueue operation enqueue, trước tiên chúng ta gọi hàm thêm, chèn một phần tử mới
ngay sau con trỏ, nghĩa là ngay sau phía sau của hàng đợi. Sau đó, chúng tôi gọi trước,
tiến con trỏ đến cái mới này , do đó làm cho nút mới ở phía sau hàng đợi. Quá trình này
được minh họa trong Hình 5.5.
Hình 5.5: Đặt "BOS" vào một hàng đợi được biểu diễn dưới dạng danh sách liên kết tròn:
(a) trước khi hoạt động; (b) sau khi thêm nút mới; (c) sau khi tiến lên
con trỏ.
Để thực hiện dequeue operation dequeue, chúng ta gọi hàm loại bỏ, do đó loại bỏ mặt
trước của hàng đợi.Quá trình này được minh họa trong Hình 5.6.
Cấu trúc lớp cho lớp kết quả, được gọi là LinkedQueue, được hiển thị trong
đoạn code 5.18. Để tránh sự lộn xộn cú pháp vốn có trong khuôn mẫu C ++
Các lớp, chúng tôi đã chọn không triển khai một lớp khuôn mẫu hoàn toàn chung chung.
Thay vì chọn định nghĩa một kiểu cho các phần tử của hàng đợi, được gọi là Elem.

Hình 5.6: Xếp hàng một phần tử (trong trường hợp này là "LAX") từ đại diện hàng đợi
phía như một danh sách liên kết thông tư: (a) trước khi hoạt động; (b) sau khi loại bỏ
ngay sau con trỏ. cấu trúc dữ liệu C. Để hỗ trợ hàm kích thước (mà CircleList không có
cung cấp), chúng tôi cũng duy trì kích thước hàng đợi trong thành viên n.
typedef string Elem; // Loại phần tử hàng đợi
class LinkedQueue { // Hàng đợi dưới dạng danh sách được liên kết gấp đôi
public:
LinkedQueue();
int size() const; // Số lượng mục trong hàng đợi
bool empty() const; // Hàng đợi có trống không?
const Elem& front() const throw(QueueEmpty); // Yếu tố phía trước
void enqueue(const Elem& e); // phần tử enqueue ở phía sau
void dequeue() throw(QueueEmpty); // Phần tử hàng đợi ở phía trước
private: // Dữ liệu thành viên
CircleList C; // Danh sách các yếu tố
int n; // Số lượng phần tử
};

Đoạn code 5.18: Lớp LinkedQueue, một triển khai dựa trên hàng đợi trên danh sách liên
kết tròn.
Trong đoạn code 5.19, chúng tôi trình bày các triển khai của hàm xây dựng và Các chức
năng phụ kiện cơ bản, kích thước, trống và mặt trước. chúng tôi tạo ra hàng đợi ban đầu
và khởi tạo n đến không. Chúng tôi không cung cấp trình hủy rõ ràng, thay vào đó dựa
vào trình hủy do CircleList cung cấp. Quan sát rằng chức năng ném một ngoại lệ nếu một
nỗ lực được thực hiện để truy cập vào phần tử đầu tiên của một hàng đợi trống. Nếu
không, nó trả về phần tử được tham chiếu bởi mặt trước của danh sách tròn, theo quy ước
của chúng tôi, cũng là yếu tố phía trước của hàng đợi.
LinkedQueue::LinkedQueue()
: C(), n(0) { }
int LinkedQueue::size() const // Số lượng mục trong hàng đợi
{ return n; }
bool LinkedQueue::empty() const // Hàng đợi có trống không?
{ return n == 0; }
// Lấy phần tử phía trước
const Elem& LinkedQueue::front() const throw(QueueEmpty) {
if (empty())
throw QueueEmpty("front of empty queue");
return C.front(); // Danh sách phía trước là hàng đợi phía trước
}
Đoạn code 5.19: Các hàm xây dựng và accessor cho lớp LinkedQueue.
Định nghĩa của các hoạt động hàng đợi, hàng đợi và hàng đợi được trình bày trong đoạn
code 5.20. Hãy nhớ lại rằng enqueuing liên quan đến việc gọi hàm add đến chèn mục mới
ngay sau con trỏ và sau đó tiến con trỏ.Trước khi xếp hàng, chúng tôi kiểm tra xem hàng
đợi có trống hay không và nếu vậy, chúng tôi loại ra. Mặt khác, dequeuing liên quan đến
việc loại bỏ phần tử ngay lập tứctheo con trỏ. Trong cả hai trường hợp, chúng tôi cập nhật
số lượng phần tử trong hàng đợi.
// phần tử enqueue ở phía sau
void LinkedQueue::enqueue(const Elem& e) {
C.add(e); // Chèn sau con trỏ
C.advance(); // . . .and advance
n++;
}
// Phần tử hàng đợi ở phía trước
void LinkedQueue::dequeue() throw(QueueEmpty) {
if (empty())
throw QueueEmpty("dequeue of empty queue");
C.remove(); // Xóa khỏi mặt trước danh sách
n−−;
}
Đoạn code 5.20: Các hàm enqueue và dequeue cho LinkedQueue.
Quan sát rằng, tất cả các hoạt động của ADT hàng đợi được thực hiện trong O(1) thời
gian. Do đó, việc thực hiện này khá hiệu quả. Không giống như dựa trên mảng Bằng cách
mở rộng, việc triển khai này sử dụng không gian tỷ lệ thuận với số lượng phần tử có
trong hàng đợi tại bất kỳ thời gian.
5.3 Kết thúc kép hàng đợi
Bây giờ hãy xem xét một cấu trúc dữ liệu giống như hàng đợi hỗ trợ chèn và xóa ở cả hai
phía trước và phía sau của hàng đợi. Phần mở rộng của hàng đợi như vậy được gọi là
double-ended queue,, hoặc deque, thường được phát âm là "boong" để tránh nhầm lẫn
với hàm dequeue của ADT hàng đợi thông thường, được phát âm như chữ viết tắt "D.Q."
5.3.1 Kiểu dữ liệu trừu tượng Deque
 Các chức năng của deque ADT như sau, trong đó D biểu thị deque:
 insertFront(e): Chèn một phần tử mới e vào đầu deque.
 insertBack(e): Chèn một phần tử mới e vào cuối deque.
 eraseFront(): Loại bỏ phần tử đầu tiên của deque; Xảy ra lỗi nếu: Deque trống
rỗng.
 eraseBack(): Loại bỏ phần tử cuối cùng của deque; Xảy ra lỗi nếu: Deque trống
rỗng.
Ngoài ra, deque bao gồm các chức năng hỗ trợ sau:
 front(): Trả về phần tử đầu tiên của deque; Xảy ra lỗi nếu: Deque trống rỗng.
 back(): Trả về phần tử cuối cùng của deque; Xảy ra lỗi nếu: Deque trống rỗng.
 size(): Trả về số lượng phần tử của deque.
 empty(): Trả về true nếu deque trống và false nếu không.
Ví dụ 5.5: Ví dụ sau đây cho thấy một loạt các hoạt động và các hiệu ứng trên một deque
trống ban đầu, D, của các số nguyên.
5.3.2 The STL Deque
Như với ngăn xếp và hàng đợi, Thư viện mẫu tiêu chuẩn cung cấp một thực hiện của một
deque. Việc triển khai cơ bản dựa trên lớp vectơ STL (Mục 1.5.5 và 6.1.4). Mô hình sử
dụng tương tự như mô hình của ngăn xếp STL và hàng đợi STL. Đầu tiên, chúng ta cần
bao gồm tệp định nghĩa "deque". Vì nó là một phần của không gian tên STD, chúng ta
cần mở đầu mỗi cách sử dụng "std: :d eque"hoặc cung cấp một tuyên bố "sử dụng" thích
hợp. Lớp deque được tạo mẫu với loại cơ sở của các yếu tố riêng lẻ. Ví dụ: đoạn mã bên
dưới tuyên bố một deque của chuỗi.
#include <deque>
using std::deque; // Làm cho Deque có thể truy cập được
deque<string> myDeque; // một đoạn của deque
Như với ngăn xếp và hàng đợi STL, một deque STL tự động thay đổi kích thước thành
các yếu tố được thêm vào.
Với những khác biệt nhỏ, lớp STL deque hỗ trợ các toán tử tương tự như giao diện của
chúng tôi. Dưới đây là danh sách các hoạt động chính.
 size(): Trả về số phần tử trong deque.
 empty(): Trả về true nếu deque trống và false nếu không.
 push front(e): Chèn e vào đầu deque.
 push back(e): Chèn e vào cuối deque.
 pop front(): Loại bỏ các yếu tố đầu tiên của deque.
 pop back(): Loại bỏ yếu tố cuối cùng của deque.
 front(): Trả về tham chiếu đến phần tử đầu tiên của deque.
 back(): Trả về tham chiếu đến phần tử cuối cùng của deque.
Tương tự như ngăn xếp và hàng đợi STL, kết quả của việc áp dụng bất kỳ thao tác nào
trước, sau, đẩy trước hoặc đẩy trở lại hàng đợi STL trống là không xác định. Vậy
Không có ngoại lệ bị loại, nhưng chương trình có thể hủy bỏ.
5.3.3 Triển khai Deque với danh sách liên kết gấp đôi
Trong phần này, chúng tôi chỉ ra cách triển khai ADT deque bằng cách sử dụng một đại
diện liên kết.Với hàng đợi, một deque hỗ trợ truy cập hiệu quả ở cả hai đầu của danh
sách, vì vậy việc triển khai của chúng tôi dựa trên việc sử dụng danh sách được liên kết
kép. Một lần nữa, chúng tôi sử dụng lớp danh sách được liên kết kép, được gọi là
DLinkedList, đã được trình bày trước đó trong Mục 3.3.3. Chúng tôi đặt mặt trước của
deque ở đầu danh sách được liên kết và phía sau hàng đợi ở cuối. Một minh họa được
cung cấp trong Hình 5.7.

Hình 5.7: Một danh sách được liên kết gấp đôi với lính canh, tiêu đề và trailer. Mặt trước
của chúng tôi deque được lưu trữ ngay sau tiêu đề ("JFK"), và mặt sau của deque của
chúng tôi được lưu trữ ngay trước đoạn giới thiệu ("SFO").
Định nghĩa của lớp kết quả, được gọi là LinkedDeque, được hiển thị trong đoạn code
5.21. Deque được lưu trữ trong thành viên dữ liệu D. Để hỗ trợ chức năng size, chúng ta
cũng duy trì Queue size trong thành viên N. Như trong một số trong các triển khai trước
đó của chúng tôi, chúng tôi tránh được sự lộn xộn cú pháp vốn có trong C ++ các lớp
mẫu, và thay vào đó chỉ sử dụng một định nghĩa kiểu để xác định cơ sở của deque loại
phần tử.
typedef string Elem; // Loại phần tử Deque
class LinkedDeque { // Deque là danh sách được liên kết gấp đôi
public:
LinkedDeque();
int size() const; // Số lượng mặt hàng trong deque
bool empty() const; // Deque có trống không?
const Elem& front() const throw(DequeEmpty); // Yếu tố đầu tiên
const Elem& back() const throw(DequeEmpty); // Yếu tố cuối cùng
void insertFront(const Elem& e); // Chèn phần tử đầu tiên mới
void insertBack(const Elem& e); // Chèn phần tử cuối cùng mới
void removeFront() throw(DequeEmpty); // Loại bỏ phần tử đầu tiên
void removeBack() throw(DequeEmpty); // Loại bỏ phần tử cuối cùng
private: // Dữ liệu thành viên
DLinkedList D; // Danh sách các yếu tố được liên kết
int n; // Số lượng phần tử
};
Đoạn code 5.21: Cấu trúc lớp cho lớp LinkedDeque.
Chúng tôi đã không bận tâm để cung cấp một trình hủy rõ ràng, bởi vì DlinkedList class
cung cấp destructor của riêng nó, được tự động gọi khi LinkedDeque bị phá hủy.Hầu hết
các chức năng thành viên cho lớp LinkedDeque đều đơn giản khái quát hóa các hàm
tương ứng của lớp LinkedQueue, vì vậy chúng ta đã bỏ qua chúng. Trong đoạn code
5.22, chúng tôi trình bày các triển khai của các chức năng thành viên để thực hiện chèn
và loại bỏ các yếu tố khỏi Deque. Quan sát rằng, trong mỗi trường hợp, chúng ta chỉ cần
gọi hoạt động thích hợp từ đối tượng DLinkedList cơ bản.
// Chèn phần tử đầu tiên mới
void LinkedDeque::insertFront(const Elem& e) {
D.addFront(e);
n++;
}
// Chèn phần tử cuối cùng mới
void LinkedDeque::insertBack(const Elem& e) {
D.addBack(e);
n++;
}
// Loại bỏ phần tử đầu tiên
void LinkedDeque::removeFront() throw(DequeEmpty) {
if (empty())
throw DequeEmpty("removeFront of empty deque");
D.removeFront();
n−−;
}
// Loại bỏ phần tử cuối cùng
void LinkedDeque::removeBack() throw(DequeEmpty) {
if (empty())
throw DequeEmpty("removeBack of empty deque");
D.removeBack();
n−−;
}
Đoạn code 5.22: Các chức năng chèn và loại bỏ cho LinkedDeque.
Bảng 5.2 cho thấy thời gian chạy của các hàm trong việc thực hiện một deque bởi một
danh sách liên kết gấp đôi.

Bảng 5.2: Hiệu suất của một deque được thực hiện bởi một danh sách liên kết kép. Việc
sử dụng không gian là O(n), trong đó n là số phần tử trong deque.
5.3.4 Bộ điều hợp và mẫu thiết kế bộ điều hợp
Việc kiểm tra các đoạn mã của Mục 5.1.5, 5.2.5 và 5.3.3, cho thấy một mô hình chung.
Trong mỗi trường hợp, chúng tôi đã lấy một cấu trúc dữ liệu hiện có và adapted được sử
dụng cho một mục đích đặc biệt. Ví dụ: trong Phần 5.3.3, chúng tôi đã chỉ ra cách thức
lớp DLinkedList của Mục 3.3.3 có thể được điều chỉnh để thực hiện một deque.Đối với
tính năng bổ sung là theo dõi số lượng phần tử, chúng tôi có chỉ cần ánh xạ từng hoạt
động deque (chẳng hạn như insertFront) đến tương ứng hoạt động của DLinkedList
(chẳng hạn như addFront).
Một adapter (còn được gọi là wrapper) là một cấu trúc dữ liệu, ví dụ, một lớp trong C+
+, dịch giao diện này sang giao diện khác.
Như một ví dụ về thích ứng, hãy quan sát rằng có thể thực hiện ngăn xếp ADT bằng cấu
trúc dữ liệu deque. Đó là, chúng ta có thể dịch từng ngăn xếp hoạt động đến một hoạt
động deque tương đương về mặt chức năng. Một ánh xạ như vậy được trình bày trong
Bảng 5.3.

Bảng 5.3: Triển khai ngăn xếp với deque.


Lưu ý rằng, vì tính đối xứng của deque, thực hiện chèn và việc di chuyển từ phía sau của
deque sẽ có hiệu quả tương đương.Tương tự như vậy, chúng ta có thể phát triển các
tương ứng cho ADT hàng đợi, như được hiển thị trong Bảng 5.4.

Bảng 5.4: Thực hiện một hàng đợi với một deque.
Như một ví dụ cụ thể hơn về mẫu thiết kế bộ điều hợp, hãy xem xét mã đoạn được hiển
thị trong đoạn code 5.23. Trong đoạn code này, chúng ta trình bày một classDequeStack,
triển khai ngăn xếp ADT. Việc thực hiện nó dựa trên dịch từng thao tác ngăn xếp sang
thao tác tương ứng trên LinkedDeque,đã được giới thiệu trong Mục 5.3.3.
typedef string Elem; // loại phần tử
class DequeStack { // Xếp chồng lên nhau như một bộ bài
public:
DequeStack();
int size() const; // Số lượng phần tử
bool empty() const; // Ngăn xếp có trống không?
const Elem& top() const throw(StackEmpty); // Yếu tố hàng đầu
void push(const Elem& e); // Đẩy phần tử lên ngăn xếp
void pop() throw(StackEmpty); // Bật ngăn xếp
private:
LinkedDeque D; // Deque của các yếu tố
};
Đoạn code 5.23: Triển khai giao diện Stack bằng deque.
Chương 7 : Cây tổng hợp
7.1.1 Định nghĩa và tính chất của cây
Cây là một kiểu dữ liệu trừu tượng lưu trữ các phần tử theo thứ bậc. Với ngoại lệ- của

phần tử trên cùng, mỗi phần tử trong cây có một phần tử cha và bằng 0 hoặc nhiều yếu tố

trẻ em hơn. Một cái cây thường được hình dung bằng cách đặt các phần tử bên trong

hình bầu dục hoặc hình chữ nhật và bằng cách vẽ ra mối liên hệ giữa cha mẹ và con cái

với những đường thẳng. Chúng ta thường gọi phần tử trên cùng là gốc của cây, nhưng nó

được coi là phần tử cao nhất, với các phần tử khác là được kết nối bên dưới (ngay đối

diện với cây thực vật).

Định nghĩa cây chính thức


Về mặt hình thức, chúng ta định nghĩa cây T là một tập hợp các nút lưu trữ các phần tử

trong phần cha-con mối quan hệ với các tính chất sau:

-Nếu T khác rỗng thì nó có một nút đặc biệt, gọi là gốc của T, không có cha mẹ.

-Mỗi nút v của T khác với nút gốc có một nút cha duy nhất w; mọi nút có cha w là con

của w.

--Lưu ý: rằng theo định nghĩa của chúng tôi, một cây có thể trống, nghĩa là nó không có

bất kỳ nút nào.Quy ước này cũng cho phép chúng ta định nghĩa một cây theo cách đệ

quy, chẳng hạn như rằng cây T trống hoặc chứa nút r, được gọi là gốc của T và một tập

hợp cây (có thể trống) có gốc là con của r.

Các mối quan hệ nút khác:


 Hai nút là con của cùng một cha mẹ là anh em. Nút v là ở ngoài nếu v

không có con.

 Nút v là nút nội nếu nó có một hoặc nhiều nút con. Bên ngoài các nút còn

được gọi là lá.

Ví dụ 7.1: Trong hầu hết các hệ điều hành, các tệp được sắp xếp theo thứ bậc thành thư

mục lồng nhau (còn gọi là thư mục), được trình bày cho người dùng dưới dạng của một

cái cây. (Xem Hình 7.3.) Cụ thể hơn, các nút bên trong của cây là được liên kết với các

thư mục và các nút bên ngoài được liên kết với các tệp thông thường. Trong hệ điều hành

UNIX và Linux, gốc của cây được đặt một cách thích hợp được gọi là “thư mục gốc” và

được biểu thị bằng ký hiệu “/”.

Hình 7.3: Cây đại diện cho một phần của hệ thống tập tin.
Nút u là tổ tiên của nút v nếu u = v hoặc u là tổ tiên của nút cha của v.

Ngược lại, chúng ta nói rằng nút v là hậu duệ của nút u nếu u là nút tổ tiên của v.

Các cạnh và đường dẫn trong cây


- Một cạnh của cây T là một cặp nút (u,v) sao cho u là cha của v, hoặc ngược lại

- Đường đi của T là một chuỗi các nút sao cho hai nút liên tiếp bất kỳ trong dãy tạo

thành một cạnh.

Cây đặt hàng


Một cây được sắp xếp nếu có thứ tự tuyến tính được xác định cho các nút con

của mỗi nút; nghĩa là, chúng ta có thể xác định các nút con của một nút là nút thứ nhất,

thứ hai, thứ ba, v.v. Thứ tự như vậy được xác định bởi cách sử dụng cây và thường được

biểu thị bằng cách vẽ cây có các cây anh em được sắp xếp từ trái sang phải, tương ứng

với mối quan hệ tuyến tính của chúng. Cây có thứ tự thường biểu thị mối quan hệ thứ tự

tuyến tính tồn tại giữa các cây anh em bằng cách liệt kê chúng theo trình tự hoặc trình

vòng lặp trong theo đúng thứ tự.


Hàm cây
Cây ADT lưu trữ các phần tử tại các nút của cây.

Thay vì, mỗi nút của cây được liên kết với một đối tượng vị trí, cung cấp công khai truy

cập vào các nút. Vì lý do này, khi thảo luận về giao diện chung của các chức năng của

ADT, chúng ta sử dụng ký hiệu p (chứ không phải v) để làm rõ rằng đối số cho chức

năng là một vị trí chứ không phải một nút. Tuy nhiên, do mối liên hệ chặt chẽ giữa hai

đối tượng này, chúng ta thường làm mờ đi sự khác biệt giữa chúng và sử dụng các thuật

ngữ “Vị trí” và “nút” thay thế cho cây.

Sức mạnh thực sự của phát hiện cây trí tuệ từ khả năng tiếp cận các vị trí lân cận

các yếu tố của cây. Cho vị trí p của cây T, ta xác định như sau:
-p.parent():Trả về cha mẹ của p; lỗi xảy ra nếu p là gốc.

-p.children():Trả về danh sách vị trí chứa nút con của nút p.Nếu p là một nút bên

ngoài thì p.children() trả về một giá trị trống danh sách.

-p.isRoot():Trả về true nếu p là gốc và ngược lại là false.

-p.isExternal():Trả về true nếu p là bên ngoài và sai nếu không.

-p.isInternal(), hàm này sẽ chỉ cần trả về phần bù của p.isExternal().

Cây cung cấp các chức năng:

- Hai hàm đầu tiên, size(kích thước) và empty(trống), chỉ là các hàm tiêu chuẩn mà

chúng ta đã xác định cho các loại vùng chứa khác mà chúng ta đã thấy.

- Hàm root mang lại vị trí của gốc và các vị trí tạo ra một danh sách chứa tất cả các

nút của cây.

-size(kích thước):Trả về số nút trong cây.

-empty(trống):Trả về true nếu cây trống và sai nếu không.

-root(nguồn gốc): Trả về vị trí cho gốc của cây, lỗi xảy ra nếu cây trống rỗng.

-positions(vị trí): Trả về danh sách vị trí của tất cả các nút của cây.

Thay vì, muốn mô tả các chức năng cập nhật cây khác nhau kết hợp với các ứng

dụng của cây ở các chương tiếp theo. Trên thực tế, chúng ta có thể tưởng tượng ra nhiều

loại các hoạt động cập nhật cây ngoài những hoạt động được đưa ra trong cuốn sách này.

7.1.3 giao diện cây C++


template <typename E> // loại phần tử cơ sở

class Position<E> { // một vị trí nút

E& operator*(); // lấy phần tử

Position parent() const; // lấy cha mẹ

PositionList children() const; // lấy con của nút


bool isRoot() const; // Nút gốc

bool isExternal() const; // nút bên ngoài?

};

Đoạn mã 7.1: Giao diện không chính thức cho một vị trí trong cây (không phải

là một giao diện hoàn chỉnh lớp C++).

template <typename E> // loại phần tử cơ sở

class Tree<E> {

public: // các loại công khai

class Position; // một vị trí nút

class PositionList; // một danh sách các vị trí

public: // chức năng công cộng

int size() const; // số nút

bool empty() const; // cây có trống không?

Position root() const; // lấy gốc

PositionList positions()const; // lấy vị trí của tất cả các nút

Đoạn mã 7.2: Giao diện không chính thức cho cây ADT (không phải là một lớp

hoàn chỉnh).

7.1.4 Cấu trúc liên kết cho cây thông thường

Chúng ta biểu diễn mỗi nút của T bởi một đối tượng vị trí p (xem Hình 7.5(a))

với các trường sau: một tham chiếu đến phần tử của nút, một liên kết tới nút cha của nút

và một số loại bộ sưu tập (ví dụ: danh sách hoặc mảng) để lưu trữ các liên kết đến nút con

của nút. Nếu p là gốc của T thì trường cha của p là NULL. T và số nút của T trong các

biến nội. được minh họa bằng sơ đồ trong Hình 7.5(b).


Hình 7.5: Cấu trúc liên kết của cây tổng quát: (a) cấu trúc nút; (b) cái phần cấu

trúc dữ liệu được liên kết với một nút và các nút con của nó.

7.2 Thuật toán truyền tải cây


Trong phần này, chúng tôi trình bày các thuật toán để thực hiện tính toán truyền

tải trên một cây bằng cách truy cập nó thông qua các hàm ADT của cây.

7.2.1 Chiều sâu và chiều cao

Cho p là một nút của cây T. Độ sâu của p là số tổ tiên của p, không bao gồm

chính p.Lưu ý rằng định nghĩa này hàm ý rằng độ sâu của nghiệm của T là 0. Độ sâu của

nút p cũng có thể được xác định đệ quy như sau:

 Nếu p là nghiệm thì độ sâu của p là 0

 Ngược lại, độ sâu của p bằng một cộng với độ sâu của phần tử cha của p

Chiều cao của nút p trong cây T cũng được xác định đệ quy.

 Nếu p ở ngoài thì chiều cao của p bằng 0

 Ngược lại, chiều cao của p bằng một cộng với chiều cao tối đa của con của p
Duyệt cây T là một cách có hệ thống để truy cập hoặc “thăm” tất cả các nút của

T. Trong phần này, chúng tôi trình bày một sơ đồ duyệt cơ bản cho cây, được gọi là duyệt

theo thứ tự trước. Trong phần tiếp theo, chúng ta nghiên cứu một sơ đồ truyền tải cơ bản

khác, được gọi là truyền tải sau thứ tự.

Trong phép duyệt cây T theo thứ tự trước, gốc của T được thăm trước và sau đó

mới đến các cây con có gốc tại các cây con của nó được duyệt đệ quy.Nếu cây được đặt

hàng thì các cây con được duyệt theo thứ tự của các cây con.Hành động cụ thể liên quan

đến “lượt truy cập” của một nút phụ thuộc vào ứng dụng của quá trình truyền tải này và

có thể liên quan đến bất cứ điều gì từ việc tăng bộ đếm đến thực hiện một số thao tác

phức tạp tính toán cho nút này. Mã giả cho việc duyệt thứ tự trước của cây con có gốc tại

nút được tham chiếu bởi vị trí p được hiển thị trong Đoạn mã 7.9. Ban đầu gọi thủ tục này

bằng lệnh gọi trước (T,T.root()).

Algorithm preorder(T, p):

perform the “visit” action for node p

for each child q of p do

recursively traverse the subtree rooted at q by calling preorder(T,q)

Đoạn mã 7.9: Thứ tự trước của thuật toán để thực hiện việc duyệt theo thứ tự

trước của cây con của cây T có gốc tại nút p.Thuật toán duyệt theo thứ tự trước rất hữu

ích trong việc tạo ra thứ tự tuyến tính của các nút của cây trong đó nút cha luôn phải đứng

trước con của họ trong thứ tự. Thứ tự như vậy có một số ứng dụng khác nhau.

Thuật toán preorderPrint(T, p), được triển khai trong C++ trong Đoạn mã 7.10,

thực hiện việc in thứ tự trước cây con của nút p của T, nghĩa là nó thực hiện duyệt cây

con có gốc tại p và in ra phần tử được lưu trữ tại nút khi nút được truy cập. Hãy nhớ lại

rằng, đối với cây có thứ tự T, hàm T.children(p) trả về một trình vòng lặp truy cập các
phần tử con của p theo thứ tự. Chúng tôi giả định rằng Iterator đây là loại vòng lặp. Cho

một iterator q, vị trí liên quan được cho bởi *q.

void preorderPrint(const Tree& T, const Position& p) {

cout << *p; // in phần tử

PositionList ch = p.children(); // danh sách nút con

for (Iterator q = ch.begin(); q != ch.end(); ++q) {

cout << " ";

preorderPrint(T, *q);

Đoạn mã 7.10: Phương thức preorderPrint(T, p) thực hiện việc in thứ tự trước

của các phần tử trong cây con tương ứng với vị trí p của T.

Có một biến thể thú vị của hàm preorderPrint tạo ra một biểu diễn khác nhau của

toàn bộ cây. Biểu diễn chuỗi ngoặc đơn P(T) của cây T được định nghĩa đệ quy như sau.

Nếu T bao gồm một nút duy nhất được tham chiếu bởi vị trí p, sau đó

P(T) = *p.

Nếu không thì,

P(T) = *p+"("+P(T1) +P(T2) +···+P(Tk)+")",

trong đó p là vị trí gốc của T và T1,T2,...,Tk là các cây con có gốc tại con của p, được sắp

xếp theo thứ tự nếu T là cây có thứ tự. Lưu ý rằng định nghĩa của P(T) là đệ quy.

Lưu ý rằng, về mặt kỹ thuật, có một số tính toán xảy ra giữa và sau các lệnh gọi

đệ quy tại các nút con trong thuật toán trên. Tuy nhiên, ta vẫn coi thuật toán này là truyền
tải theo thứ tự trước, vì thuật toán chính hành động in nội dung của nút xảy ra trước các

lệnh gọi đệ quy.

Hàm parenPrint trong C++, được trình bày trong Đoạn Mã 7.11, là một biến thể

của chức năng preorderPrint (Đoạn mã 7.10). Nó thực hiện định nghĩa được đưa ra ở trên

để xuất ra một biểu diễn chuỗi ngoặc đơn của cây T. Đầu tiên, nó in phần tử liên kết với

mỗi nút. Đối với mỗi nút bên trong, trước tiên chúng tôi in “(”, theo sau bằng cách biểu

diễn trong ngoặc đơn của mỗi phần tử con của nó, theo sau là “)”.

void parenPrint(const Tree& T, const Position& p) {

cout << *p; // in phần tử của nút

if (!p.isExternal()) {

PositionList ch = p.children(); // danh sách nút con

cout << "( "; // mở

for (Iterator q = ch.begin(); q != ch.end(); ++q) {

if (q != ch.begin()) cout << " "; // in dấu phân cách

parenPrint(T, *q); // nút con tiếp theo

cout << " )"; // đóng

Đoạn mã 7.11: Triển khai thuật toán parenPrint trong C++.

7.2.3 Truyền tải thứ tự sau


Một thuật toán duyệt cây quan trọng khác là duyệt theo thứ tự sau. Thuật toán

này có thể được coi là đối lập với việc duyệt theo thứ tự trước, bởi vì nó đệ quy duyệt qua
các cây con có gốc tại các cây con trước tiên, sau đó mới đến thăm gốc.Tuy nhiên, nó

tương tự như việc duyệt thứ tự trước ở chỗ chúng ta sử dụng nó để giải quyết một vấn đề

cụ thể. vấn đề bằng cách chuyên biệt hóa một hành động liên quan đến “lượt truy cập”

của nút p.

Tuy nhiên, giống như việc duyệt theo thứ tự trước, nếu cây được sắp thứ tự,

chúng ta thực hiện các lệnh gọi đệ quy cho các nút con của nút p theo thứ tự được chỉ

định của chúng. Mã giả cho truyền tải sau thứ tự được đưa ra trong Đoạn mã 7.12.

Algorithm postorder(T, p):

for each child q of p do

recursively traverse the subtree rooted at q by calling postorder(T,q)

perform the “visit” action for node p

Đoạn mã 7.12: Thuật toán postorder để thực hiện việc duyệt postorder cây con của cây T

có gốc tại nút p.

Trong Đoạn mã 7.13, chúng tôi trình bày một hàm C++ postorderPrint thực hiện

việc duyệt thứ tự sau của cây T. Hàm này in phần tử được lưu trữ tại một nút khi nó được

truy cập.

void postorderPrint(const Tree& T, const Position& p) {

PositionList ch = p.children(); // danh sách nút con

for (Iterator q = ch.begin(); q != ch.end(); ++q) {

postorderPrint(T, *q);

cout << " ";

cout << *p; } // in phần tử

Đoạn mã 7.13: Hàm postorderPrint(T, p), in ra các phần tử của cây con ở vị trí p của T.
Phương pháp duyệt thứ tự sau rất hữu ích để giải quyết các bài toán trong đó

chúng ta muốn tính một số thuộc tính cho mỗi nút p trong cây, nhưng việc tính thuộc tính

đó cho p yêu cầu chúng ta phải tính cùng thuộc tính đó cho các nút con của p. Một ứng

dụng như vậy được minh họa trong ví dụ sau.

Ví dụ 7.7: Xét cây hệ thống tệp T, trong đó các nút bên ngoài biểu thị các tệp và

các nút bên trong biểu thị các thư mục (Ví dụ 7.1). Giả sử chúng ta muốn tính toán dung

lượng ổ đĩa được sử dụng bởi một thư mục, được tính đệ quy bằng tổng của các giá trị

sau (xem Hình 7.9):

 Kích thước của chính thư mục

• Kích thước của các tập tin trong thư mục

• Không gian được sử dụng bởi các thư mục con

Hình 7.9: Cây trong Hình 7.3 biểu thị một hệ thống tệp, hiển thị tên và kích

thước của tệp/thư mục được liên kết bên trong mỗi nút và không gian đĩa được sử dụng

bởi thư mục liên kết phía trên mỗi nút bên trong.
7.3 Cây nhị phân
Cây nhị phân là cây có thứ tự trong đó mỗi nút có nhiều nhất hai nút con.

1. Mỗi nút có tối đa hai nút con.

2. Mỗi nút con được gắn nhãn là nút con trái hoặc nút con phải.

3. Con trái đứng trước con phải theo thứ tự các con của một nút.

Cây con có gốc ở nút con trái hoặc phải của nút bên trong được gọi tương ứng là

cây con trái hoặc cây con phải của nút đó. Cây nhị phân là phù hợp nếu mỗi nút có 0

hoặc 2 nút con. Một số người cũng coi những cây như vậy là cây nhị phân đầy đủ. Vì

vậy, trong một cây nhị phân thích hợp, mỗi nút bên trong có đúng hai nút con. Cây nhị

phân không phù hợp là không phù hợp.

• Nếu một nút là bên ngoài thì giá trị của nó là biến hoặc hằng.

• Nếu một nút là bên trong thì giá trị của nó được xác định bằng cách áp dụng

phép toán của nó cho các giá trị của các nút con của nó.

Cây biểu thức số học là cây nhị phân thực sự, vì mỗi toán tử +, −, × và / lấy

chính xác hai toán hạng. Tất nhiên, nếu chúng ta cho phép các toán tử một ngôi, như

phủ định (-), như trong “−x,” thì chúng ta có thể có một cây nhị phân không đúng.
Hình 7.11: Cây nhị phân biểu diễn một biểu thức số học. Cây này biểu thị biểu

thức ((((3+1)×3)/((9−5)+2))−((3×(7−4))+6)). Giá trị được liên kết với nút bên trong có

nhãn “/” là 2.

Định nghĩa cây nhị phân đệ quy

Ngẫu nhiên, chúng ta cũng có thể định nghĩa cây nhị phân theo cách đệ quy sao cho cây

nhị phân trống hoặc bao gồm:

• Nút r, gọi là gốc của T và lưu trữ một phần tử

• Cây nhị phân, gọi là cây con trái của T

• Cây nhị phân, gọi là cây con bên phải của T

7.3.1 Cây nhị phân ADT


Trong phần này, chúng tôi giới thiệu kiểu dữ liệu trừu tượng cho cây nhị phân.

Giống như ADT cây trước đây của chúng ta, mỗi nút của cây lưu trữ một phần tử và được

liên kết với đối tượng vị trí, cung cấp quyền truy cập công cộng vào các nút. Bằng cách

nạp chồng toán tử tham chiếu, phần tử được liên kết với vị trí p có thể được truy cập bởi

*p. Ngoài ra, vị trí p còn hỗ trợ các thao tác sau.

p.left(): Trả về con trái của p; một tình trạng lỗi xảy ra nếu p là một nút bên

ngoài.

p.right(): Trả về con phải của p; một tình trạng lỗi xảy ra nếu p là một nút bên

ngoài.

p.parent(): Trả về cha của p; lỗi xảy ra nếu p là gốc.

p.isRoot(): Trả về true nếu p là gốc và ngược lại là false.

p.isExternal(): Trả về true nếu p là bên ngoài và sai nếu không.

Cây cũng cung cấp các hoạt động tương tự như cây tiêu chuẩn ADT. Như size(),

empty(), root(), positions().


7.3.2 Giao diện cây nhị phân C++
Ta trình bày một giao diện C++ không chính thức cho cây nhị phân ADT. Bắt

đầu trong Đoạn mã 7.15 bằng cách trình bày một giao diện C++ không chính thức cho

lớp Vị trí, đại diện cho một vị trí trong cây. Nó khác với giao diện cây ở Phần 7.1.3 ở chỗ

thay thế hàm thành viên cây con bằng hai hàm left và right.

template <typename E> // loại phần tử cơ sở

class Position<E> { // một vị trí nút

public:

E& operator*(); // lấy phần tử

Position left() const; // lấy con trái

Position right() const; // lấy con phải

Position parent() const; // lấy cha mẹ

bool isRoot() const; // nút gốc?

bool isExternal() const; // một nút bên ngoài?

};

Đoạn mã 7.15: Giao diện không chính thức cho cây nhị phân ADT (không phải

là lớp C++ hoàn chỉnh).

Tiếp theo, trong Đoạn mã 7.16, trình bày giao diện C++ không chính thức cho

cây nhị phân. Để giữ cho giao diện đơn giản nhất có thể, ta bỏ qua việc xử lý lỗi và do đó

ta không khai báo bất kỳ ngoại lệ nào sẽ được đưa ra.

template <typename E> // loại phần tử cơ sở

class BinaryTree<E> { // cây nhị phân

public: // public types

class Position; // một vị trí nút


class PositionList; // một danh sách các vị trí

public: // member functions

int size() const; // số nút

bool empty() const; // cây có trống không?

Position root() const; // lấy nút gốc

PositionList positions() const; // danh sách các nút

};

Cây nhị phân có một số thuộc tính thú vị liên quan đến mối quan hệ giữa chiều

cao và số nút của chúng. Chúng ta biểu thị tập hợp tất cả các nút của cây T, ở cùng độ sâu

d, là mức d của T. Trong cây nhị phân, mức 0 có một nút (gốc), cấp 1 có nhiều nhất hai

nút (con của nút gốc), cấp 2 có nhiều nhất là bốn nút, v.v. Nói chung, cấp d có nhiều nhất

là các nút 2d.

Mệnh đề 7.10: Cho T là cây nhị phân khác rỗng và gọi n, nE, nI và h lần lượt là số

nút, số nút ngoài, số nút trong và chiều cao của T. Khi đó T có các tính chất sau:

1. h+1 ≤ n ≤ 2h+1 −1

2. 1 ≤ nE ≤ 2h

3. h ≤ nI ≤ 2h −1

4. log(n+1)−1 ≤ h ≤ n−1

Ngoài ra, nếu T đúng thì nó có các thuộc tính sau:

1. 2h+1 ≤ n ≤ 2h+1 −1

2. h+1 ≤ nE ≤ 2h

3. h ≤ nI ≤ 2h −1

4. log(n+1)−1 ≤ h ≤ (n−1)/2
Hình 7.12: Số nút tối đa ở các cấp của cây nhị phân.

Ngoài các thuộc tính của cây nhị phân ở trên, chúng ta còn có mối quan hệ sau

đây giữa số lượng nút bên trong và nút bên ngoài trong một cây nhị phân thích hợp.

Mệnh đề 7.11: Trong cây nhị phân T khác rỗng, số nút bên ngoài nhiều hơn số nút bên

trong một.

Giải thích: Chúng ta có thể thấy điều này bằng cách sử dụng lập luận dựa trên quy nạp.

Nếu cây bao gồm một nút gốc duy nhất thì rõ ràng chúng ta có một nút bên ngoài và

không có nút bên trong nào, do đó mệnh đề đúng. Mặt khác, nếu chúng ta có hai hoặc

nhiều hơn thì gốc có hai cây con. Vì các cây con nhỏ hơn cây ban đầu nên chúng ta có thể

giả định rằng chúng thỏa mãn mệnh đề. Như vậy, mỗi cây con có nhiều hơn một nút bên

ngoài so với các nút bên trong. Giữa hai nút này có nhiều nút bên ngoài hơn hai nút bên

trong. Tuy nhiên, gốc của cây là một nút bên trong. Khi chúng ta xem xét gốc và cả hai

cây con cùng nhau, sự khác biệt giữa số lượng nút bên ngoài và bên trong là 2−1 = 1.
7.3.4 Cấu trúc liên kết cho cây nhị phân

Trong phần này, cách triển khai cây nhị phân T dưới dạng cấu trúc được liên kết,

được gọi là LinkedBinaryTree. Chúng ta biểu diễn mỗi nút v của T bằng một đối tượng

nút lưu trữ phần tử liên quan và các con trỏ tới nút cha và hai nút con của nó. (Xem Hình

7.13.) Để đơn giản, chúng ta giả sử cây là đúng, nghĩa là mỗi nút có 0 hoặc 2 nút con.

Hình 7.13: Một nút trong cấu trúc dữ liệu được liên kết để biểu diễn cây nhị phân.

Chúng ta bắt đầu bằng việc xác định các thành phần cơ bản tạo nên lớp

LinkedBinaryTree. Thực thể cơ bản nhất là cấu trúc Nút, được hiển thị trong đoạn mã sau

struct Node { // một nút của cây

Elem elt; // giá trị phần tử

Node* par; // nút cha

Node* left; // nút con trái

Node* right; // nút con phải

Node() : elt(), par(NULL), left(NULL), right(NULL) { } // xây dựng

};

Đoạn mã 7.17: Nút cấu trúc thực hiện một nút của cây nhị phân. Nó được lồng

trong phần được bảo vệ của lớp BinaryTree.


class Position { // vị trí trên cây

private:

Node* v; // con trỏ tới nút

public:

Position(Node* v = NULL) : v( v) { } // constructor

Elem& operator*() // lấy phần tử

{ return v−>elt; }

Position left() const // lấy nút con trái

{ return Position(v−>left); }

Position right() const // lấy nút con phải

{ return Position(v−>right); }

Position parent() const // lấy nút cha

{ return Position(v−>par); }

bool isRoot() const // nút gốc?

{ return v−>par == NULL; }

bool isExternal() const // nút bên ngoài?

{ return v−>left == NULL && v−>right == NULL; }

friend class LinkedBinaryTree; // cấp quyền truy cập cho cây

};

typedef std::list<Position> PositionList; // danh sách các vị trí

Đoạn Mã 7.18: Lớp Vị trí thực hiện một vị trí trong cây nhị phân. Nó được lồng trong

phần công khai của lớp Cây nhị phân được liên kết.

Hầu hết các hàm của lớp Vị trí chỉ đơn giản liên quan đến việc truy cập các

thành viên thích hợp của cấu trúc Nút. Chúng ta cũng đã bao gồm một khai báo về lớp
Danh sách vị trí, dưới dạng danh sách các vị trí STL. Danh sách này được sử dụng để thể

hiện các tập hợp các nút. Để giữ cho mã đơn giản, chúng tôi đã bỏ qua việc kiểm tra lỗi

và thay vì sử dụng các mẫu, chúng tôi chỉ cung cấp định nghĩa kiểu cho loại phần tử cơ

sở, được gọi là Elem.

Dữ liệu riêng cho lớp LinkedBinaryTree bao gồm một con trỏ gốc tới nút gốc và

một biến n, chứa số lượng nút trong cây.Ngoài các chức năng của ADT, còn có một số

hàm cập nhật, addRoot, ExpandExternal và RemoveAboveExternal, cung cấp các phương

tiện để xây dựng và sửa đổi cây.

LinkedBinaryTree::LinkedBinaryTree() // constructor

:_root(NULL), n(0) { }

int LinkedBinaryTree::size() const // number of nodes

{ return n; }

bool LinkedBinaryTree::empty() const // is tree empty?

{ return size() == 0; }

LinkedBinaryTree::Position LinkedBinaryTree::root() const // get the root

{ return Position( root); }

void LinkedBinaryTree::addRoot() // add root to empty tree

{_root = new Node; n = 1; }

Đoạn mã 7.20: Các hàm thành viên đơn giản cho lớp LinkedBinaryTree.

Hàm cập nhật cây nhị phân


Ngoài các hàm giao diện BinaryTree và addRoot, lớp LinkedBi-naryTree còn

bao gồm các hàm cập nhật sau đây với vị trí p. Cái đầu tiên được sử dụng để thêm các nút

vào cây và cái thứ hai được sử dụng để loại bỏ các nút.
expandExternal(p): Chuyển đổi p từ một nút bên ngoài thành một nút bên trong

bằng cách tạo hai nút bên ngoài mới và biến chúng lần lượt là con trái và con phải của p;

một điều kiện lỗi xảy ra nếu p là một nút bên trong.

RemoveAboveExternal(p): Loại bỏ nút bên ngoài p cùng với nút cha q của nó,

thay thế q bằng nút anh em của p (xem Hình 7.15, trong đó nút của p là w và nút của q là

v); tình trạng lỗi xảy ra nếu p là nút bên trong hoặc p là nút gốc.

Hình 7.15: Thao tác RemoveAboveExternal(p), loại bỏ nút bên ngoài w mà p

tham chiếu tới và nút cha v của nó.

// expand external node

void LinkedBinaryTree::expandExternal(const Position& p) {

Node* v = p.v; // p’s node

v−>left = new Node; // add a new left child

v−>left−>par = v; // v is its parent

v−>right = new Node; // and a new right child

v−>right−>par = v; // v is its parent

n += 2; // two more nodes

Đoạn mã 7.21: Hàm mở rộngExternal(p) của lớp LinkedBinaryTree.


Hàm ExpandExternal(p) được hiển thị trong Đoạn mã 7.21. Đặt v là nút liên kết

của p, nó tạo ra hai nút mới. Một đứa trở thành con trái của v và đứa kia trở thành con

phải của v. Hàm tạo của Node khởi tạo các con trỏ của nút thành NULL, vì vậy chúng ta

chỉ cần cập nhật các liên kết chính của nút mới.

Hiệu suất triển khai LinkedBinaryTree


Phân tích thời gian chạy của các hàm của lớp LinkedBinaryTree, lớp này sử

dụng biểu diễn cấu trúc được liên kết.

• Mỗi hàm vị trí trái, phải, cha, isRoot và isExternal mất O(1) thời gian.

• Bằng cách truy cập vào biến thành viên n, biến này lưu trữ số lượng nút của T, kích

thước hàm và làm trống mỗi lần chạy trong thời gian O(1).

• Root hàm truy cập chạy trong thời gian O(1).

• Các hàm cập nhật ExpandExternal và RemoveAboveExternal chỉ truy cập một số nút

không đổi, vì vậy cả hai đều chạy trong thời gian O(1).

• Vị trí hàm được thực hiện bằng cách thực hiện duyệt theo thứ tự trước, mất O(n) thời

gian. Các nút được duyệt qua cây nhị phân được thêm vào danh sách STL trong thời gian

O(1). Do đó, vị trí hàm mất O(n) thời gian.


7.3.5 Cấu trúc dựa trên vectơ cho cây nhị phân
Cấu trúc đơn giản để biểu diễn cây nhị phân T dựa trên cách đánh số các nút của

T. Với mọi nút v của T, cho f(v) là số nguyên được xác định như sau:

• Nếu v là nghiệm của T thì f(v) = 1

• Nếu v là nút con trái của nút u thì f(v) = 2 f(u)

• Nếu v là nút con bên phải của nút u thì f(v) = 2 f(u) +1
Hình 7.17: Biểu diễn cây nhị phân T bằng vectơ S.
7.3.6 Truyền cây nhị phân
Giống như cây thông thường, việc tính toán trên cây nhị phân thường liên quan
đến việc duyệt.
 Duyệt trước cây nhị phân
 Truyền tải thứ tự sau của cây nhị phân
Cây tìm kiếm nhị phân
Cho S là một tập hợp có các phần tử có quan hệ thứ tự. Ví dụ: S có thể là một tập hợp các
số nguyên. Cây tìm kiếm nhị phân cho S là cây nhị phân thích hợp T sao cho:
• Mỗi nút bên trong p của T lưu trữ một phần tử của S, ký hiệu là x(p)
• Với mỗi nút trong p của T, các phần tử lưu trong cây con bên trái của p nhỏ hơn hoặc
bằng x(p) và các phần tử lưu trong cây con bên phải của p lớn hơn hoặc bằng x(p)
Các nút bên ngoài của T không lưu trữ bất kỳ phần tử nàoViệc duyệt theo thứ tự
các nút bên trong của cây tìm kiếm nhị phân T sẽ truy cập các phần tử theo thứ tự không
giảm. (Xem Hình 7.19.)
Hình 7.19: Cây tìm kiếm nhị phân lưu trữ các số nguyên. Đường nét đứt màu xanh
lam được đi ngang khi tìm kiếm (thành công) cho 36. Đường nét đứt màu xanh lam được
đi ngang khi tìm kiếm (không thành công) cho 70.
Sử dụng phương pháp truyền tải thứ tự để vẽ cây
Việc duyệt theo thứ tự cũng có thể được áp dụng cho bài toán tính toán bản vẽ cây
nhị phân. Chúng ta có thể vẽ cây nhị phân T bằng thuật toán gán tọa độ x và y cho nút p
của T bằng hai quy tắc sau (xem Hình 7.20).
• x(p) là số nút được truy cập trước p trong lần duyệt thứ tự của T.
• y(p) là độ sâu của p trong T.

Hình 7.20: Thuật toán vẽ thứ tự cho cây nhị phân.


7.3.7 Mẫu hàm mẫu
Mẫu hàm mẫu mô tả một cơ chế tính toán chung có thể chuyên biệt cho một ứng
dụng cụ thể bằng cách xác định lại các bước nhất định
Euler Tour với mẫu hàm mẫu: Theo mẫu hàm mẫu, chúng ta có thể thiết kế một
thuật toán, template-EulerTour, thực hiện hành trình duyệt Euler chung của cây nhị phân.
Khi được gọi trên nút p, hàm templateEulerTour gọi một số hàm phụ trợ khác ở các giai
đoạn khác nhau của quá trình truyền tải. Trước hết, nó tạo ra cấu trúc 3 phần tử r để lưu
trữ kết quả tính toán gọi hàm phụ trợ initRe-sult. Tiếp theo, nếu p là một nút bên ngoài,
templateEulerTour gọi hàm phụ trợ VisitExternal, nếu không (p là một nút bên trong)
templateEulerTour thực hiện các bước sau:
• Gọi hàm phụ trợ VisitLeft, thực hiện các phép tính liên quan đến việc gặp nút
bên trái
• Gọi đệ quy chính nó ở con bên trái
• Gọi hàm phụ trợ VisitBelow, thực hiện các tính toán liên quan đến việc gặp nút
từ bên dưới
• Gọi đệ quy chính nó trên cây con bên phải
• Gọi hàm phụ trợ VisitRight, thực hiện các phép tính liên quan đến việc gặp nút
bên phải
Cuối cùng, templateEulerTour trả về kết quả tính toán bằng cách gọi hàm phụ trợ
returnResult. Mẫu hàm EulerTour có thể được xem như một khuôn mẫu hoặc “bộ khung”
của một chuyến tham quan Euler. (Xem Đoạn Mã 7.30.)
Algorithm templateEulerTour(T, p):
r ← initResult()
if p.isExternal() then
r.finalResult ← visitExternal(T, p,r)
else
visitLeft(T, p,r)
r.leftResult ← templateEulerTour(T, p.left())
visitBelow(T, p,r)
r.rightResult ← templateEulerTour(T, p.right())
visitRight(T, p,r)
return returnResult(r)
Đoạn mã 7.30: Hàm templateEulerTour để tính toán truyền tải Eulertour
tổng quát của cây con của cây nhị phân T bắt nguồn từ nút p, theo mẫu hàm mẫu.
Hàm này gọi các hàm initResult, VisitExter-nal, VisitLeft, VisitBelow, VisitRight
và ReturnResult.
Trong ngữ cảnh hướng đối tượng, chúng ta có thể viết một lớp EulerTour
như sau:
• Chứa hàm templateEulerTour
• Chứa tất cả các hàm phụ trợ được gọi bởi templateEulerTour dưới dạng
phần giữ chỗ trống (nghĩa là không có hướng dẫn hoặc trả về NULL)
• Chứa một hàm thực thi gọi templateEulerTour(T,T.root())
Bản thân lớp EulerTour không thực hiện bất kỳ tính toán hữu ích nào. Tuy
nhiên, chúng ta có thể mở rộng nó bằng cơ chế kế thừa và ghi đè các hàm trống để
thực hiện các tác vụ hữu ích.
Ví dụ về hàm mẫu
• Mở rộng lớp EulerTour
• Ghi đè hàm initResult bằng cách trả về một mảng gồm ba số
• Ghi đè hàm VisitExternal bằng cách trả về giá trị được lưu tại nút
• Ghi đè hàm VisitRight bằng cách kết hợp r.leftResult và r.rightResult với
toán tử được lưu trữ tại nút và đặt r.finalResult bằng kết quả của thao tác
• Ghi đè hàm returnResult bằng cách trả về r.finalResult Cách tiếp cận này
nên được so sánh với việc triển khai trực tiếp thuật toán được hiển thị trong Đoạn
mã 7.26. Ví dụ thứ hai, chúng ta có thể in biểu thức liên kết với cây biểu thức số
học bằng cách sử dụng lớp PrintExpression mới:
• Mở rộng lớp EulerTour
• Ghi đè hàm VisitExternal bằng cách in giá trị của biến hoặc hằng số liên
kết với nút
• Ghi đè chức năng VisitLeft bằng cách in “(”
• Ghi đè hàm VisitBelow bằng cách in toán tử liên kết với nút
• Ghi đè chức năng VisitRight bằng cách in “)”
7.3.8 Biểu diễn cây tổng quát bằng cây nhị phân
Một biểu diễn thay thế của cây tổng quát T thu được bằng cách chuyển đổi
T thành cây nhị phân T. (Xem Hình 7.22.) Chúng ta giả sử rằng T được sắp xếp
thứ tự hoặc nó được sắp xếp tùy ý. Sự chuyển đổi như sau:
• Với mỗi nút u của T, có một nút bên trong u′ của T′ liên kết với u
• Nếu u là một nút ngoài của T và không có nút anh em ngay sau nó thì các
nút con của u′ trong T′ là các nút ngoài
• Nếu u là nút trong của T và v là con đầu tiên của u trong T thì v′
là con trái của bạn' ở T
• Nếu nút v có anh em w ngay sau nó thì w′ là con phải của v′ trong T
Lưu ý rằng các nút bên ngoài của T’ không được liên kết với các nút của T
và chỉ đóng vai trò giữ chỗ (do đó, thậm chí có thể rỗng).

Hình 7.22: Biểu diễn cây bằng cây nhị phân: (a) cây T; (b) cây nhị phân T’ liên kết
với T. Các cạnh nét đứt kết nối các nút của T’ liên kết với các nút anh em của T.
Dễ dàng duy trì sự tương ứng giữa T và T', và biểu diễn các phép toán trong T
dưới dạng các phép toán tương ứng trong T'. Một cách trực quan, chúng ta có thể nghĩ về
sự tương ứng dưới dạng chuyển đổi T thành T′ lấy mỗi tập hợp anh chị em {v1,v2,...,vk}
trong T với cha mẹ v và thay thế nó bằng một chuỗi con bên phải có gốc tại v1, sau đó trở
thành con trái của v.

Phần 2 : ĐỀ TÀI QUẢN LÝ SINH VIÊN


I. Lí do chọn đề tài
1.Mục đích của việc viết chương trình quản lý sinh viên là nhằm:

 Hỗ trợ công tác quản lý sinh viên của nhà trường: Chương trình quản lý sinh
viên giúp nhà trường tự động hóa các quy trình quản lý, đồng thời giúp nhà
trường dễ dàng truy cập và xử lý thông tin, từ đó đưa ra các quyết định chính
xác và kịp thời.
 Nâng cao chất lượng giáo dục: Chương trình quản lý sinh viên giúp nhà
trường theo dõi sát sao tình hình học tập của sinh viên. Từ đó có thể cải
thiện chất lượng giảng dạy.
 Tăng cường giao tiếp giữa nhà trường, sinh viên và phụ huynh: Chương
trình quản lý sinh viên giúp nhà trường, sinh viên và phụ huynh dễ dàng trao
đổi thông tin với nhau

Tóm lại, chương trình quản lý sinh viên là một công cụ hữu ích giúp nhà trường và
sinh viên quản lý và theo dõi hiệu quả quá trình học tập và giáo dục.

2.Nhu cầu của nhà trường và lợi ích của sinh viên

-Nhu cầu của nhà trường:

 Cải thiện hiệu quả quản lý: Chương trình quản lý sinh viên giúp nhà trường
tự động hóa các quy trình quản lý, từ đó tiết kiệm thời gian và công sức cho
nhân viên.
 Nâng cao chất lượng giáo dục: Chương trình giúp nhà trường thu thập dữ
liệu về kết quả học tập của sinh viên, từ đó có thể cải thiện chất lượng giảng
dạy.
 Tăng cường giao tiếp giữa nhà trường, sinh viên và phụ huynh: Chương
trình quản lý sinh viên giúp nhà trường, sinh viên và phụ huynh dễ dàng trao
đổi thông tin với nhau, từ đó tăng cường sự gắn kết giữa các bên
- Lợi ích của sinh viên:

+ Tăng cường khả năng tự quản: Chương trình quản lý sinh viên giúp sinh viên
dễ dàng truy cập và cập nhật thông tin học tập, từ đó giúp sinh viên tự quản lý thời
gian và học tập hiệu quả hơn.
+Tăng cường sự chủ động: Chương trình quản lý sinh viên giúp sinh viên chủ
động đăng ký môn học, lớp học, thời khóa biểu,..., từ đó giúp sinh viên chủ
động hơn trong việc học tập.

+Tăng cường sự kết nối: Chương trình quản lý sinh viên giúp sinh viên dễ dàng
kết nối với nhau, với giảng viên và với nhà trường, từ đó tạo ra một môi trường
học tập tích cực và hiệu quả hơn.

II. Mô tả thuật toán quản lý sinh viên

1. Khai Báo Cấu Trúc:

- Đầu tiên, chương trình định nghĩa một cấu trúc `sinhvien` để lưu trữ thông tin của
sinh viên bao gồm mã sinh viên, tên, lớp, giới tính và điểm trung bình.

- Tiếp theo, định nghĩa một cấu trúc `node` để tạo ra các nút trong danh sách liên
kết chứa dữ liệu sinh viên và con trỏ đến nút tiếp theo.

- Định nghĩa cấu trúc `list` để đại diện cho danh sách liên kết với con trỏ đầu và
cuối.

2. Tạo Danh Sách:

- Sử dụng hàm `CreatList` để tạo một danh sách liên kết mới bằng cách thiết lập
con trỏ đầu và cuối là NULL.

3. Nhập Thông Tin Sinh Viên:

- Sử dụng hàm `CreateNode` để tạo một nút mới chứa thông tin sinh viên bằng
cách đọc dữ liệu từ người dùng và trả về con trỏ đến nút đó.

- Hàm `input` được sử dụng để nhập thông tin cho `n` sinh viên bằng cách tạo nút
và thêm chúng vào danh sách.

4. Thêm Sinh Viên vào Danh Sách:

- Có hai hàm `addhead` và `addtail` để thêm một nút vào đầu hoặc cuối của danh
sách liên kết.
5. Sắp Xếp Danh Sách:

- Sử dụng thuật toán sắp xếp nổi bọt để sắp xếp danh sách theo mã sinh viên.

6. Xóa Sinh Viên:

- Có hai hàm `xoacuoi` và `xoadau` để xóa sinh viên ở cuối và đầu danh sách.

7. Tìm Kiếm Sinh Viên:

- Sử dụng hàm `timkiem` để tìm kiếm sinh viên trong danh sách theo mã sinh viên.

8. Hủy Danh Sách:

- Sử dụng hàm `huydanhsach` để giải phóng bộ nhớ của toàn bộ danh sách.

9. Menu và Lựa Chọn:

- Chương trình chạy trong một vòng lặp vô hạn, hiển thị một menu cho người dùng
lựa chọn các chức năng như nhập thông tin sinh viên, xuất thông tin, kiểm tra trùng
lặp mã sinh viên, thêm và xóa sinh viên, tìm kiếm sinh viên và hủy danh sách.

III. Nội dung lý thuyết

1. Giới Thiệu về Quản Lý Sinh Viên:

Quản lý sinh viên là một hệ thống giúp tổ chức giáo dục lưu trữ, xử lý và quản lý
thông tin của sinh viên một cách hiệu quả. Hệ thống này thường bao gồm các chức
năng như thêm, xóa, sửa, tìm kiếm thông tin sinh viên, và thường được sử dụng ở
các trường đại học, trung học phổ thông và các tổ chức giáo dục khác.

2. Cần Thiết của Hệ Thống Quản Lý Sinh Viên:

- Tối ưu hóa quy trình: Hệ thống quản lý sinh viên giúp tối ưu hóa quy trình quản
lý thông tin, giúp giảm công sức và thời gian của nhân viên quản lý.

- Minh bạch và chính xác: Thông tin trong hệ thống phải được lưu trữ một cách
minh bạch và chính xác, giúp tránh những sai sót đáng tiếc trong quản lý sinh viên.
- Dễ Dàng Tìm Kiếm và Truy Cập: Hệ thống cần cung cấp khả năng tìm kiếm
nhanh chóng và truy cập dễ dàng đến thông tin của sinh viên, bao gồm cả thông tin
cá nhân và học vụ.

3. Ưu Điểm của Việc Sử Dụng Ngôn Ngữ Lập Trình C++:

- Hiệu Suất Cao: C++ là một ngôn ngữ lập trình hiệu quả với hiệu suất cao, điều
này có nghĩa là hệ thống quản lý sinh viên viết bằng C++ sẽ chạy nhanh và đáng
tin cậy.

- Kiểm Soát Bộ Nhớ: Sử dụng C++ cho phép quản lý tốt việc sử dụng bộ nhớ,
giúp tránh lãng phí tài nguyên và tối ưu hóa hiệu suất.

- Đa Nhiệm và Linh Hoạt: C++ hỗ trợ đa nhiệm và có tính linh hoạt cao, cho phép
dễ dàng xây dựng các chức năng phức tạp trong hệ thống quản lý sinh viên.

4. Quy Trình Phát Triển Hệ Thống Quản Lý Sinh Viên:

- Thu Thập Yêu Cầu: Xác định các yêu cầu cụ thể của hệ thống, bao gồm các chức
năng cần thiết, yêu cầu về giao diện người dùng và yêu cầu về dữ liệu.

- Thiết Kế Hệ Thống: Thiết kế cấu trúc dữ liệu, giao diện người dùng và các chức
năng của hệ thống quản lý sinh viên.

- Lập Trình và Kiểm Thử: Viết mã nguồn bằng C++, sau đó tiến hành kiểm thử để
đảm bảo rằng hệ thống hoạt động đúng như mong đợi và không có lỗi.

- Triển Khai và Duy Trì: Triển khai hệ thống trên môi trường thực tế và tiếp tục
duy trì, cập nhật hệ thống để đáp ứng các yêu cầu mới và sửa các lỗi phát sinh.

5. Kết Luận:

Việc sử dụng ngôn ngữ lập trình C++ để xây dựng hệ thống quản lý sinh viên
không chỉ mang lại hiệu suất và hiệu quả cao mà còn giúp tạo ra một ứng dụng linh
hoạt và dễ dàng mở rộng theo thời gian. Bằng cách sử dụng kiến thức về lập trình
và quản lý dữ liệu, các nhà phát triển có thể xây dựng những hệ thống quản lý sinh
viên mạnh mẽ và đáng tin cậy.
IV. Các thuật toán và cấu trúc dữ liệu được áp dụng

Struct
truct sinhvien {
int ma;
string name;
string lop;
string gioitinh;
float gpa;
};

struct node {
sinhvien data;
node *next;
};

struct list {
node *head;
node *tail;
};

void CreatList(list &l) {


l.head = l.tail = NULL;
}
Hàm kiemtratrungma

bool kiemtratrungma(list l){


for(node *p=l.head;p!=l.tail;p=p->next){
for(node *q=p->next;q!=NULL;q=q->next){
if(p->data.ma==q->data.ma){
cout<<"da bi trung:";
return true;}
Hàm createNode
node* CreateNode(sinhvien* &x) {
node *p = new node;
x = new sinhvien;

cout << "Ma sinh vien:";


cin >> x->ma;
cin.ignore();
cout << "Ho ten:";
getline(cin, x->name);

cout << "Lop hoc:";


getline(cin, x->lop);

cout << "Gioi tinh:";


getline(cin, x->gioitinh);

cout << "Diem tong ket:";


cin >> x->gpa;

p->data = *x;
p->next = NULL;
return p;
Hàm main()
int main() {
iist(l);
sinhvien *x;

while (true) {
cout << "\n\t=====MENU======";
cout << "\n1. nhap thong tin sinh vien:";
cout << "\n2. xuat thong tin sinh vien:";
cout << "\n3. kiem tra ma so sinh vien co bi trung truoc khi them
khong:";
cout << "\n4. them sinh vien vao dau:";
cout << "\n5. them sinh vien vao cuoi:";
cout << "\n6. sap xep sinh vien theo ma sinh vien:";
cout << "\n7. xoa sinh vien dau danh sach:";
cout << "\n8. xoa sinh vien cuoi danh sach:";
cout << "\n9. tim kiem sinh vien theo ma sinh vien:";
cout <<"\n10. tim kiem sinh vien theo ten";
cout<< "\n11. kiem tra hoc bong cua tung sinh vien";
cout << "\n12. huy toan bo danh sach sinh vien:";
cout << "\n13. thoat.";

int chon;
cout << "\n\t===>moi chon so:";
cin >> chon;

switch (chon) {
case 1:
cout << "nhap so luong sinh vien:";
cin >> n;
input(l, n);
break;

case 2:
output(l);
break;

case 3:
kiemtratrungma(l);
break;

case 4:{
node* p = CreateNode(x);
cout << "sau khi them sinh vien dau danh sach la:";
addhead(l, p);
output(l);
break;
}

case 5:
{
node* p = CreateNode(x);
cout << "sau khi them sinh vien vao cuoi danh sach la:";
addtail(l, p);
output(l);
break;
}

case 6:
cout << "sau khi sap xep la:\n";
sapxep(l);
output(l);
break;

case 7:
cout << "danh sach sinh vien sau khi xoa:\n";
xoadau(l);
output(l);
break;

case 8:
cout << "danh sach sau khi xoa:\n";
xoacuoi(l);
output(l);
break;

case 9:
cout << "sau khi tim kiem la:\n";
timkiem(l);
break;
case 10:
{
cin.ignore();
string name;
cout<<"Nhap ten cua sinh vien can tim kiem: ";
getline(cin,name);
searchByName(l,name);
break;
}

case 11:
checkScholarship(l);
break;

case 12:
cout << "sau khi da huy moi nhap lai tu dau:\n";
huydanhsach(l);
break;

case 13:
cout << "thoat!\n";
return 0;

default:
cout << "Lua chon khong hop le!\n";
break;
}

You might also like