Professional Documents
Culture Documents
QLSV N13
QLSV N13
QLSV N13
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.
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.
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.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
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
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
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à
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.
- Đườ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
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
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
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.
- 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
-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.
};
Đ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
class Tree<E> {
Đ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).
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
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ó.
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.
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
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.
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
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
Đ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
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.
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
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à “)”.
if (!p.isExternal()) {
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.
Đ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
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.
postorderPrint(T, *q);
Đ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
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ị
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.
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ị
• 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.
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
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.
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(),
đầ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.
public:
};
Đ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
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 đó
};
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
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
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
};
Đ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
private:
public:
{ return v−>elt; }
{ return Position(v−>left); }
{ return Position(v−>right); }
{ return Position(v−>par); }
};
Đ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ơ
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
LinkedBinaryTree::LinkedBinaryTree() // constructor
:_root(NULL), n(0) { }
{ return n; }
{ return size() == 0; }
Đoạn mã 7.20: Các hàm thành viên đơn giản cho lớp LinkedBinaryTree.
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.
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.
• 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).
• 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
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à 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.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.
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
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.
- Đầ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.
- 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.
- 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.
- 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.
- Có hai hàm `xoacuoi` và `xoadau` để xóa sinh viên ở cuối và đầu danh sách.
- Sử dụng hàm `timkiem` để tìm kiếm sinh viên trong danh sách theo mã sinh viên.
- Sử dụng hàm `huydanhsach` để giải phóng bộ nhớ của toàn bộ danh sách.
- 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.
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.
- 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ụ.
- 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.
- 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;
};
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;
}