Đối việc hiển thị các thông tin lựa chọn lên giao diện, ta xem màn hình đang hiển thị một Menu
. Như vậy, một Menu
có thể sẽ bị xếp chồng lên bởi một Menu
khác và màn hình hiện tại chỉ hiển thị Menu
đang ở trên đỉnh.
Để cài đặt, ta định nghĩa 2 lớp là Menu
(định nghĩa một Menu
- bao gồm các lựa chọn và tiêu đề) và MenuStack
(chứa một ngăn xếp các Menu
). Các phương thức trên lớp MenuStack
có thể kể đến như:
push(Menu)
: thêm một Menu
lên đỉnh ngăn xếppop()
: xoá bỏ đi Menu
đang ở đỉnh ngăn xếptop()
: lấy ra Menu
đang ở đỉnh ngăn xếp (để hiển thị)getChoice()
: yêu cầu người dùng nhập vào một chọn lựa và trả về số ứng với chọn lựa (đã bao gồm việc kiểm tra hợp lệ)Cài đặt của 2 lớp này có thể tìm thấy ở file mã nguồn Menu.h
.
Việc định nghĩa ra các Menu
và sử dụng chúng sẽ được thể hiện ở file mã nguồn main.cpp
. Ở đây các Menu
bao gồm: Đăng nhập, Chọn mục cần quản trị, Quản lý sách, Quản lý độc giả, Quản lý thủ thư, Quản lý mượn-trả và các Menu
tìm kiếm.
Việc luân chuyển giữa các Menu
thực hiện bằng câu lệnh switch-case
, goto
kết hợp phương thức getChoice()
.
Ở mã nguồn trên, sau khi hiển thị Menu
trên cùng của ngăn xếp MenuStack
(bằng lệnh cout << menuStack;
), ta sẽ dựa vào lựa chọn trả về từ phương thức getChoice()
mà sẽ lựa chọn đẩy Menu
nào tiếp theo vào ngăn xếp (phương thức push()
) và di chuyển đến nhãn tương ứng (lệnh goto
).
Bảng băm là một cấu trúc dữ liệu quan trọng để phục vụ việc tối ưu hoá các truy vấn tìm kiếm, thêm mới, sửa đổi. Ta có thể ứng dụng trong việc lưu trữ các thông tin như: thông tin sách, thông tin mượn-trả hoặc thông tin độc giả. Do được sử dụng ở nhiều nơi và liên quan đến nhiều lớp, cũng như việc lựa chọn khoá để băm cũng phụ thuộc vào ngữ cảnh bài toán. Ta sẽ dựng một lớp chung để cài đặt cấu trúc dữ liệu này (có sử dụng generic).
Một trong những yếu tố quan trọng để xây dựng bảng băm là phải xây dựng hàm băm. Hàm băm sẽ quyết định vị trí lưu trữ của dữ liệu (ở đây là kiểu dữ liệu generic <T>
) trong bảng băm.
Việc lựa chọn hàm băm sẽ được quyết định tại lúc tạo ra một đối tượng của lớp HashTableLL
. Hai con trỏ hàm hash
và isEqual
sẽ trỏ đến hai hàm tương ứng là hàm băm khoá hash
dựa trên giá trị có kiểu T
- trả về một số nguyên ứng với giá trị băm được và hàm kiểm tra bằng nhau isEqual
- kiểm tra xem khoá trong giá trị đứng trước và giá trị đứng sau trong tham số (đều có kiểu T
) có thực sự bằng nhau hay không.
Việc kiểm tra này là cực kỳ cần thiết trong trường hợp tìm kiếm từ bảng băm, đặc biệt là khi xác suất xảy ra đụng độ luôn tồn tại.
Đối với giải thuật được cài trong lớp HashTableLL
, việc xử lý đụng độ được dựa trên giải pháp sử dụng danh sách liên kết (hai chữ cái LL
đại diện cho danh sách liên kết đơn - (single) Linked List). Như vậy, ta xem mỗi dòng trong bảng băm là một danh sách liên kết các Node
, mà bảng băm là một mảng các Node
nên sẽ có kiểu dữ liệu con trỏ cấp hai Node**
(bảng sẽ có size
dòng). Các phương thức insert(T)
(thêm một giá trị mới vào bảng băm), remove(T)
(xoá một giá trị ra khỏi bảng băm) và search(T)
đều có sử dụng đến hàm hash(T)
được truyền vào; hàm isEqual
được sử dụng trong phương thức remove
để đảm bảo xoá chính xác giá trị (khoá đúng với khoá cần xoá) và search(T)
để đảm bảo tìm kiếm chính xác các giá trị chứa khoá cần tìm.
Đối với phương thức search(T)
, ta giả định rằng sẽ có nhiều giá trị mang cùng một khoá nên sẽ trả về không, một hoặc nhiều giá trị tìm được trong một mảng.
Cài đặt của lớp này có thể tìm thấy ở file mã nguồn HashTableLL.h
.
Đối với từng kho chứa dữ liệu (Repository) - cụ thể là BookRepository
(chứa thông tin của tất cả sách hiện có), ReaderRepository
(chứa thông tin của tất cả độc giả) và BorrowingRepository
(chứa thông tin của tất cả lượt mượn-trả sách) - ta sẽ tạo ra một hoặc một số bảng băm ứng với khoá là thuộc tính đang quan tâm. Ví dụ, đối với BookRepository
, ta quan tâm việc tối ưu tìm kiếm theo tên tác giả hoặc mã định danh của sách. Do đó, ta sẽ tạo ra 2 bảng băm là booksByAuthorTable
(ứng với hàm băm khoá là hashAuthor
, hàm so sánh kiểm tra trùng khớp là isEqualAuthor
) và booksByIdTable
(hàm băm khoá là hashId
, hàm so sánh kiểm tra trùng khớp là isEqualId
).
Giải thuật trong các hàm băm khoá được sử dụng trong bài cũng hết sức đơn điệu, đó là cộng các ký tự để lấy tổng các giá trị mã hoá ký tự (theo bảng mã ASCII) và trả về tổng đó. Ví dụ đơn giản với hàm isEqualAuthor
và hàm isEqualAuthor
:
Cài đặt của các lớp này có thể tìm thấy ở các file mã nguồn Book.h
, Reader.h
và Borrowing.h
.
Ngoài ra, bảng băm còn được sử dụng trong file mã nguồn Librarian.h
để phục vụ tìm kiếm nhanh thủ thư để thực hiện bước đăng nhập hệ thống. Tuy nhiên, nếu số lượng thủ thư ít thì việc tối ưu này cũng không mang lại nhiều lợi ích mà ngược lại còn gây hao tổn không ít không gian bộ nhớ để chứa cả bảng băm.
Song song với việc tìm kiếm (ví dụ như phương thức getBookById(string)
, getBooksByAuthor(string)
,…), việc thực hiện các thao tác thêm hay xoá cũng sẽ làm ảnh hưởng đến dữ liệu trên bảng băm. Do đó, trên các phương thức như addBook(...)
, removeBook(string)
,… ta cũng sẽ phải xử lý cập nhật trên các bảng băm tương ứng. Dưới đây là minh hoạ cho các phương thức kể trên.
Để minh hoạ cho cách hoạt động, ta giả sử có 3 cuốn sách và một bảng băm có kích thước là 5 (chỉ số dòng là 0, 1, 2, 3, 4) và 3 quyển sách có mã định danh lần lượt là:
BOOK00001
: giá trị băm được là 66+79+79+75+48+48+48+49=492
, vị trí trong bảng băm là 492 % 5 = 2
BOOK00002
: giá trị băm được là 66+79+79+75+48+48+48+50=493
, vị trí trong bảng băm là 493 % 5 = 3
BOOK00010
: giá trị băm được là 66+79+79+75+48+48+49+48=492
, vị trí trong bảng băm là 492 % 5 = 2
Vậy sau khi thêm tất cả vào bảng băm booksByIdTable
, ta được kết quả như sau:
Ngoài việc sử dụng bảng băm, ta có thể sử dụng thêm cây nhị phân cân bằng AVL để tối ưu hoá thao tác tìm kiếm. Trong bài này, ta cũng sẽ cài đặt cây AVL dưới dạng sử dụng generic và cung cấp sẵn một số phương thức trên cây.
Cây AVL sử dụng chiều cao để cân bằng, do đó các thông tin cơ bản trong một nút (Node
) bao gồm: value
(giá trị tại nút), left
(nút bên trái), right
(nút bên phải) và height
(chiều cao nút hiện tại).
Tương tự như cài đặt của HashTableLL
, các phương thức cơ bản phải có của lớp AVLTree
bao gồm: phương thức insert(T)
(thêm một giá trị mới vào cây - ứng với việc tạo mới một nút), remove(T)
(xoá một giá trị ra khỏi cây - ứng với việc xoá đi một nút) và search(T)
(so sánh và tìm ra một hoặc một số giá trị (nút) thoả điều kiện bằng của hàm so sánh compare(T, T)
truyền vào lúc tạo ra AVLTree
).
Ngoài ra các phương thức hỗ trợ như tính toán chiều cao của nút height(Node*)
, tính độ chênh lệch chiều cao tại nút (bằng chiều cao cây con trái trừ chiều cao cây con phải) getBalance(Node*)
, xoay trái leftRotate(Node*)
, xoay phải rightRotate(Node*)
, thêm nút vào cây insert(Node*, T)
, xoá nút khỏi cây remove(Node*, T)
, tìm nút con bé nhất (nút trái cùng) minValueNode(Node*)
cũng được cài đặt để hỗ trợ các phương thức chính phía trên.
Cài đặt của lớp này có thể tìm thấy ở file mã nguồn AVLTree.h
.
Cây AVL được sử dụng ở 2 kho chứa là BookRepository
(phục vụ tìm kiếm theo tên sách) và ReaderRepository
(phục vụ tìm kiếm theo tên độc giả). Ứng với đó là 2 phương thức so sánh compareTitle(Book*, Book*)
và compareReaderByName(Reader*, Reader*)
:
Ở trong hai phương thức so sánh này có sử dụng đến hàm toLower()
(có thể tìm thấy trong file mã nguồn StringHelper.h
) để chuyển đổi chuỗi về dạng in thường, từ đó cho phép tìm kiếm không phân biệt hoa thường.
Đương nhiên, việc thêm hay xoá dữ liệu sách, độc giả cũng làm cây phải cập nhật. Do đó trong các phương thức như addBook(...)
, removeBook(string)
,… cũng sẽ phải gọi đến các phương thức cập nhật của cây.
Cài đặt của các lớp này có thể tìm thấy ở các file mã nguồn Book.h
và Reader.h
.
Để minh hoạ cho cách hoạt động, ta giả sử có 3 cuốn sách có tên lần lượt là:
ABC
AAC
ACD
ACE
Vậy sau khi thêm tất cả vào cây AVL booksByTitleTree
theo thứ tự, ta được kết quả như sau:
Nếu ta thêm một cuốn sách có tiêu đề là ADE
, cây sẽ bị mất cân bằng tại nút ACD
và phép xoay trái xảy ra tại nút này để cây cân bằng lại như sau: