圖書信息管理系統
一、數據結構分析
答:(1)線性表:適用於數據元素類型相同且數據元素之間存在線性關系;應用:圖書管理系統,稀疏多項式的運算。(2)棧:數據元素可以是任意類型的數據,但必須是同一個數據對象,棧中數據元素之間也是線性關系,特點是先進后出;應用:括號匹配,表達式求值。(3)隊列:隊列和棧類似,是一種特殊的線性表,但隊列的特點是先進先出;應用:排隊匹配,迷宮求解。(4)樹:樹型結構是一種非線性結構,樹的結點之間有分支並具有層次關系;應用:二叉樹,哈夫曼樹等。(5)圖:結點之間的關系可以是任意。應用:構造最小生成樹,尋找最短路徑。
本題,圖書管理系統中,我選擇線性表。理由:每本書都有相同的數據元素,可以把圖書信息表抽象成一個線性表,每一本書作為線性表的一個元素。
二、存儲結構分析
采用順序存儲結構還是鏈式存儲結構?分析順序結構和鏈式結構分別適合用於什么樣的場景,在本題中選擇哪種存儲結構,並說明原因。
答:圖書管理系統中,如果圖書數據較多,需要頻繁的進行插入和刪除操作可采用鏈表,若圖書數據個數變化不大,很少進行插入和刪除則可采用順序表。此題,我選擇雙鏈表。理由:我以文本BookLis.txt中讀取書籍信息,數據較多所以選擇鏈表,同時,我以冒泡排序法對書籍排序,在鏈表結點比較時需要比較多輪,所以選擇雙鏈表,方便回溯到表頭。
三、存儲結構的實現
1、符號常量定義
#define OK 1
#define ERROR 0
typedef int Status;
2、類型定義
(1)鏈表結點定義
typedef struct LNode
{
Book data;
struct LNode *next;
struct LNode *prior;
}LNode, *LinkList;
(2)對Book類型定義
typedef struct Book
{
char bookname[30];
char ISBN[30];
char price[10];
char author[30];
char press[30];//出版社
char time[15];//出版時間,例如2019.06.18
char vetrsion[10];//版本
char page[10];
char flag;//狀態,判斷是否可借
int mark = 0;//標記
}Book;
(3)符號重載
a: 對輸入符“>>”重載:
friend istream &operator >> (istream &i, Book& b)
{
cout << "請輸入書號ISBN:"; i >> b.ISBN;
cout << "請輸入書名:"; i >> b.bookname;
cout << "請輸入價格:"; i >> b.price;
cout << "請輸入作者:"; i >> b.author;
cout << "請輸入出版時間:"; i >> b.time;
cout << "請輸入出版社:"; i >> b.press;
cout << "請輸入版本號:"; i >> b.vetrsion;
cout << "請輸入頁數:"; i >> b.page;
cout << "狀態(1可借),(0不可借):"; i >> b.flag;
cout << "\n";
return i;
}
b: 對輸出符“<<”重載:
friend ostream &operator << (ostream &i, Book& b)
{
cout << "+——————————————+\n";
cout << "|ISBN:" << setfill(' ') << setw(15) << b.ISBN << setfill(' ') << setw(8) << "|" << endl;
cout << "|" << setfill(' ') << setw(29) << "|" << endl;
cout << "|書名:" << setfill(' ') << setw(15) << b.bookname << setfill(' ') << setw(8) << "|" << endl;
cout << "|" << setfill(' ') << setw(29) << "|" << endl;
cout << "|作者:" << setfill(' ') << setw(15) << b.author << setfill(' ') << setw(8) << "|" << endl;
cout << "|" << setfill(' ') << setw(29) << "|" << endl;
cout << "|價格:" << setfill(' ') << setw(15) << b.price << setfill(' ') << setw(9) << "|" << endl;
cout << "|" << setfill(' ') << setw(29) << "|" << endl;
cout << "|頁數:" << setfill(' ') << setw(15) << b.page << setfill(' ') << setw(8) << "|" << endl;
cout << "|" << setfill(' ') << setw(29) << "|" << endl;
cout << "|版本:" << setfill(' ') << setw(15) << b.vetrsion << setfill(' ') << setw(8) << "|" << endl;
cout << "|" << setfill(' ') << setw(29) << "|" << endl;
cout << "|出版時間:" << setfill(' ') << setw(15) << b.time << setfill(' ') << setw(5) << "|" << endl;
cout << "|" << setfill(' ') << setw(29) << "|" << endl;
cout << "|出版社:" << setfill(' ') << setw(15) << b.press << setfill(' ') << setw(6) << "|" << endl;
cout << "|" << setfill(' ') << setw(29) << "|" << endl;
if (b.flag == '0')
cout << "|狀態:" << setfill(' ') << setw(15) << "已借出" << setfill(' ') << setw(8) << "|" << endl;
else
cout << "|狀態:" << setfill(' ') << setw(15) << "可借" << setfill(' ') << setw(8) << "|" << endl;
cout << "+——————————————+\n";
return i;
}
說明:對“<<”重載時使用了<iomanip>庫中的setfill(),setw()函數,目的是格式化輸出書籍信息,更加工整美觀。
四、基本操作概要設計
1、InitList(LinkList &L)
操作結果:構造一個空的雙鏈表L
2、CreateList(LinkList &L)
初始條件:雙鏈表L已存在
操作結果:從BookList.txt文件中讀取書籍數據存入雙鏈表L
3、ListInsert(LinkList &L)
說明:使用后插法插入雙鏈表L
初始條件:雙鏈表L已存在
操作結果:插入一個新結點於表L尾部
4、ListRevise(LinkList &L, char ISBN[])
初始條件:雙鏈表L已存在,用戶輸入ISBN號
操作結果:修改ISBN號對應書籍的數據
5、ListDelete(LinkList &L, char ISBN[])
初始條件:雙鏈表L已存在,用戶輸入ISBN號
操作結果:刪除ISBN號對應書籍的數據
6、LocateList(LinkList L, int oper)
初始條件:雙鏈表L已存在,用戶輸入oper查找方式和查找關鍵字
操作結果:返回關鍵字對應書籍數據
說明:oper提供4種查找方式:
<1>按ISBN查找 <2>按作者查找 <3>按書名查找 <4>按出版社查找
7、OrderList(LinkList &L, int oper, int i)
初始條件:雙鏈表L已存在,用戶輸入oper排序方式
操作結果:雙鏈表L按oper方式排序
說明:oper提供4種排序方式:
<1>按書名排序 <2>按價格排序 <3>按ISBN排序 <4>按出版時間排序
8、ReturnBook(LinkList L)
初始條件:雙鏈表L已存在,用戶輸入ISBN
操作結果:對應ISBN書籍狀態更新
9、BorrowBook(LinkList L)
初始條件:雙鏈表L已存在,用戶輸入ISBN
操作結果:對應ISBN書籍狀態更新
10、CountList(LinkList L, int oper)
初始條件:雙鏈表L已存在,用戶輸入oper計數方式
操作結果:返回按oper計數結果
說明:oper提供3種計數方式:
<1>按出版社計數 <2>按作者計數 <3>按狀態計數
11、DisplayList(LinkList L)
初始條件:雙鏈表L已存在
操作結果:遍歷雙鏈表L顯示每個結點數據
12、SaveData(LinkList L)
初始條件:雙鏈表L已存在
操作結果:打開BookList.txt文件覆寫數據,
說明:程序退出時自動調用SaveData更新數據
五、基本操作詳細設計
1、創建鏈表
創建鏈表L時直接從文件BookList.txt文件中讀入數據,免去了需要用戶輸入大量書籍信息的工作。
Status CreateList(LinkList &L)
{
fstream file;
file.open("BookList.txt");
if (!file) {
cout << "錯誤!未找到文件!\n\n" << endl;
exit(ERROR);
}
LNode *p, *r;
r = L;
while (file.peek() != EOF) {
p = new LNode;
file >> p->data.ISBN >> p->data.bookname >> p->data.price
>> p->data.author >> p->data.time >> p->data.press
>> p->data.vetrsion >> p->data.page >> p->data.flag;//“>>”提取符
p->prior = r;
p->next = NULL;
r->next = p;
r = p;
}
L->prior = r;
r->next = L;
return OK;
}
BookList.txt文件:
1111 數據結構 53.00元 嚴蔚敏 2018.03.12 人民出版社 第3版 239頁 1
2222 程序設計 48.00元 馮希 2003.04.14 郵電出版社 第4版 513頁 1
3333 前端開發 62.00元 樓浩仁 2014.09.05 郵電出版社 第3版 384頁 1
4444 大學英語 35.00元 蔣官東 2006.12.09 重慶出版社 第8版 473頁 1
5555 高等數學 59.00元 康峰 2007.04.31 人民出版社 第7版 743頁 1
6666 近代史 81.00元 何虎 2001.06.21 重慶出版社 第5版 634頁 0
7777 線性代數 47.00元 蔣文 2017.03.24 人民出版社 第5版 634頁 1
注:(每本書的元素之間用空格隔開,兩本書之間可以不用換行或空格)
óóó原因:
(1)提取符“>>”從file中提取各種數據。“>>”會以空格為分隔符逐個從文件中讀取數據(>>會忽略空格和換行符)並將其保存到相應的數據變量中,若沒有空格數據會依次讀入變量中直到變量空間已滿或者遇到EOF文件結束符時才會停止,所以同一本書的不同元素之間需要以空格隔開。
(2)兩本書之間可以不用換行或空格,以下面為例:
…… 人民出版社 第3版 239頁 12222 程序設計 48.00元 馮驥 ……
每本書最后一個數據為char flag單個字符變量,當“>>”讀取字符 “1”或者“0”后,flag空間已滿,“>>”仍可以讀取后面“2222”數據但不能存入flag中,只能等到下一次存入新的一本書的第一個變量中(即ISBN)。因此仍可以正常讀入數據
2、 鏈表排序
對鏈表排序時,涉及中文漢字排序。通過一個小項目test測試中文字符比較大小:
int main()
{
char a[30] = "人民教育出版社";
char b[30] = "郵電出版社";
char c[30] = "重慶出版社";
if (strcmp(a, b) < 0)
cout << b << "大";
if (strcmp(a, c) > 0)
cout << c << "小";
if (strcmp(b, c) < 0)
cout << b << "小";
return 0;
}
這里發現strcmp函數比較中文字符時,根據第一個字符首字母比較大小,然后依次后移進行比較。所以排序時以strccmp函數比較關鍵字大小。每一輪比較結束后需要指針p需要回溯到表頭L為下一輪比較做准備,因為L為雙鏈表可直接p=p->next,只需要判斷一輪是否結束。此處按書名排序為例:
LNode *p = L->next, *q = L->next;
if (!p)
return 0;
int flag = 1;
if (oper == 1) {//按書名排序
while (flag && i >= 0) {//冒泡排序
flag = 0;
while (p->next != L) {
if (strcmp(p->data.bookname, p->next->data.bookname) > 0) {
LNode *q;
q = new LNode;
q->data = p->next->data;
p->next->data = p->data;
p->data = q->data;
flag = 1;//標志置為1表示本輪有交換
}
p = p->next;
}
--i;
p = L->next;//重新指向首元結點為下輪准備
}
}
3、 統計計數功能
例如:用戶要求按“<1>出版社”計數,我們需要找到關鍵字出版社的名字並對鏈表進行遍歷,找到關鍵字相同的結點時計數器+1,然后進行第二次遍歷,這是關鍵字已經不再是上一次出版社的名字了,而是另一個與前一次不同的出版社的名字,這里的難點就是我們不知道出版社應該有幾類,也不知道要遍歷多少次才能全部統計結束。因此,我在Book類型里面添加了一個標記int mark表示已經遍歷並且計數成功時,mark由0(未讀)置為1(已讀)。第一次遍歷時將第一個結點mark置為1並strcpy關鍵字到一個新的字符串數組中,一輪遍歷結束后要對關鍵字進行更新並繼續遍歷,直到全部結點mark為1或者遍歷指針q=L時統計完成。
if (oper == 1) {//按出版社計數
while (q != L) {
char press[30] = { 0 };
int num = 1;
while (p != L && p->data.mark == 1) {//p遍歷找到未讀標記
p = p->next;
q = p;//下一次從p(未讀)開始
}
if (q == L)//全部統計結束
break;
strcpy_s(press, p->data.press);//更新關鍵字
p->data.mark = 1;
while (p != L) {
if (!strcmp(press, p->next->data.press)) {
++num;
p->next->data.mark = 1;
}
p = p->next;
}
q = q->next;
p = q;
cout << "------------------------\n";
cout << press << " " << num << "本" << endl;
}
óóó這里需要注意!!!
如果用戶要求再次統計時,會統計失敗。原因是統計結束后鏈表結點的mark值全部為1,再次統計時無法找到標記值為0的結點,所以每次統計結束后需要重置mark為0,才能夠保證再次統計,問題解決。
p = L->next;
while (p != L) {
p->data.mark = 0;
p = p->next;
++total;//書籍總數
}
4、 關於查找問題
鏈表的查找很簡單,就是通過關鍵字遍歷匹配,找到關鍵字相匹配的結點,返回該結點並結束。剛開始我也是這樣用的,然而實際情況還需要多考慮一下。例如:按作者查找書籍,關鍵字“劉雲傑”,那么遍歷時我們會找到與“劉雲傑”匹配成功的結點並返回它,查找結束。如果“劉雲傑”寫了多本書呢?這種情況我們始終只能找到第一本匹配成功的書而無法確定后續結點是否也滿足。針對這點不足,需要對LocateList改寫:
while (p != L) {
if (!strcmp(author, p->data.author))
cout << p->data;
p = p->next;
}
5、新增借還功能
在Book類型里面增加了char flag狀態標志,1為可借,0為已借出。用戶按照ISBN號查找書籍並完成借還。這里有一個錯誤控制,即書籍狀態為0時,會有提示該書為不可借狀態。
Status BorrowBook(LinkList L)//借書
{
cout << "請輸入ISBN:";
char newISBN[30];
cin >> newISBN;
LNode *p;
p = L->next;
while (p != L && strcmp(newISBN, p->data.ISBN))
p = p->next;
if (p == L)
p = NULL;
else if (p->data.flag == '0')
{//判斷書籍狀態
cout << "借書失敗,該書為不可借狀態!\n";
return ERROR;
}
else
p->data.flag = '0';
return OK;
}
Status ReturnBook(LinkList L)//還書
{
cout << "請輸入ISBN:";
char newISBN[30];
cin >> newISBN;
LNode *p;
p = L->next;
while (p != L && strcmp(newISBN, p->data.ISBN))
p = p->next;
if (p == L)
p = NULL;
else
p->data.flag = '1';
return OK;
}
6、保存數據
在退出程序時對鏈表的數據進行覆寫。我們保存數據后,第二次運行程序需要重新讀入數據,前面提到元素之間必須要有空格,這時必須要保證上一次程序結束時,覆寫的數據必須符合讀入的格式。這里用fprintf(fp1, " ")來保證元素之間的空格,fprintf(fp1, "\n")對每一本書的數據進行換行寫入文本來保證格式工整。
Status SaveData(LinkList L)
{
LNode *p;
p = L->next;
FILE* fp1;
errno_t err;
err = fopen_s(&fp1, "BookList.txt", "w");
if (err != 0) {
printf("文件打開失敗!\n");
exit(0);
}
while (p != L) {
fputs(p->data.ISBN, fp1);
fprintf(fp1, " ");//元素之間用空格隔開,這里必須有空格
fputs(p->data.bookname, fp1);
fprintf(fp1, " ");
fputs(p->data.price, fp1);
fprintf(fp1, " ");
fputs(p->data.author, fp1);
fprintf(fp1, " ");
fputs(p->data.time, fp1);
fprintf(fp1, " ");
fputs(p->data.press, fp1);
fprintf(fp1, " ");
fputs(p->data.vetrsion, fp1);
fprintf(fp1, " ");
fputs(p->data.page, fp1);
fprintf(fp1, " ");
fputc(p->data.flag, fp1);
if (p->next != L)//每本書之間換行再寫入,當然也可以不用換行,不影響讀入
fprintf(fp1, "\n");//換行是為了文本數據工整美觀
p = p->next;
}
fclose(fp1);
return OK;
}
óóó關於換行
這里為什么要p->next != L 才換行,也就是說為什么在最后一本書寫入完成之后不能換行?經過我的測試,如果在最后一本書換行,第二次再讀入文件時會多讀入一次數據,而且顯示時會多出一本書,這本書的信息為空白。
if (p->next != L)//每本書之間換行再寫入,當然也可以不用換行,不影響讀入
fprintf(fp1, "\n");//換行是為了文本數據工整美觀
原因:while (file.peek() != EOF)中peek()會預讀下一個字符,在最后一本書寫入完成之后,如果添加了換行符(或者空格),那么再次打開程序進行讀入文件時,會將最后一行的換行符或者空格也讀入鏈表中(在這里多出一本空白書),然后才是讀入文件結束符EOF結束。前面提到“>>”自動忽略空格和換行符,那么在最后一行的換行符為什么沒有被忽略而是被讀入了呢?
網上查閱資料:
(1)“>>”操作符會忽略前面的空白符和換行符,但不會越過后面的換行符和空白符
(2)get()方法不會略過任何符號
(3)peek()方法預讀取下一個字符(不管是何符號)
六、程序總體設計
1、構造鏈表。
(1) InitList(L):初始化鏈表L
(2) CreateList(L):
從BookList.txt文件中讀取書籍信息存入鏈表中。
鏈表構造成功,書籍信息被錄入鏈表中。
2、功能選擇:
七、主程序
主程序的實現代碼。注意給出注釋。
int main()
{/********標題***********/
cout << "***********************************************************************************************" << endl;
cout << "*** xx ***-----****----***** 圖書管理系統 *****-----****----*** xx ***" << endl;
cout << "*** xx ***-----****----***** 圖書管理系統 *****-----****----*** xx ***" << endl;
cout << "*** xx ***-----****----***** 圖書管理系統 *****-----****----*** xx ***" << endl;
cout << "***********************************************************************************************" << endl;
/*********************構造鏈表********************/
LinkList L;
InitList(L);
CreateList(L);//從BookList.txt文件中讀取書籍信息
/*********************功能選擇********************/
char ch;
while (1)
{
cout << "++-------------------------------------------------------------------------------------------------------------++\n";
cout << "||請選擇要進行的操作:<a>顯示 <b>查找 <c>修改 <d>刪除 <e>統計計數 <o>排序 <i>插入 <j>借還 <n>退出 ||\n";
cout << "++-------------------------------------------------------------------------------------------------------------++\n";
ch = _getwch();
if (ch == 'a')//顯示
DisplayList(L);
else if (ch == 'b') {//查找
cout << "++----------------------------------------------------------------------------------++\n";
cout << "||請選擇要進行的操作:<1>按ISBN查找 <2>按作者查找 <3>按書名查找 <4>按出版社查找||\n";
cout << "++----------------------------------------------------------------------------------++\n";
int oper;
cin >> oper;
LocateList(L, oper);
}
else if (ch == 'c') {//修改
cout << "請輸入需要修改書籍的ISBN號." << endl;
char ISBN[30];
cin >> ISBN;
ListRevise(L, ISBN);
}
else if (ch == 'd') {//刪除
cout << "請輸入需要刪除書籍的ISBN號." << endl;
char ISBN[30];
cin >> ISBN;
if (ListDelete(L, ISBN))
cout << "刪除成功!\n";
else
cout << "刪除失敗!\n";
}
else if (ch == 'e') {//統計計數
int oper;
cout << "<1>按出版社計數 <2>按作者計數 <3>按狀態計數 " << endl;
cin >> oper;
CountList(L, oper);
}
else if (ch == 'j') {//結束、還書
cout << "<b> 借書 <r> 還書\n";
char x;
cin >> x;
if (x == 'b' && BorrowBook(L))
cout << "借書成功!\n";
else if (x == 'r'&& ReturnBook(L))
cout << "還書成功!\n";
}
else if (ch == 'o'){//排序
cout << "+---------------------------------------------------------------------------------+\n";
cout << "|請選擇排序方式:<1>按書名排序 <2>按價格排序 <3>按ISBN排序 <4>按出版時間排序 |\n";
cout << "+---------------------------------------------------------------------------------+\n";
int oper, i = 0;
cin >> oper;
LNode *q = L->next;
while (q != L) {
q = q->next;
++i;//數據節點個數i
}
if (OrderList(L, oper,i))
cout << "排序成功!\n";
else
cout << "排序失敗!\n";
}
else if (ch == 'i') {
if (ListInsert(L))
cout << "插入成功!\n";
else
cout << "插入失敗!\n";
}
else if (ch == 'n') {//退出
SaveData(L);
break;
}
}
}
八、編譯調試
由於要求函數部分做成lib,可在此處介紹如何生成lib的過程,以及在工程中怎么調用自定義的lib文件中的函數。
1、創建靜態庫項目
2、添加文件
在解決方案中添加 .h頭文件和對應的 .cpp文件,頭文件中聲明函數和常量定義,cpp文件中給出函數的具體實實現。
3、解決方案
這樣會生成一個.lib文件和.h文件,這樣就完成了一個靜態庫。
4、調用lib
然后我們可以把.lib文件和.h文件放到我們的項目中,這樣就可以使用自己
靜態庫函數了,當然也可以不拷貝文件,只要我們的程序中寫明.lib文件的
路徑,也是同樣可以使用的。
九、總結
這次的作業讓我實現了人生第一個600行代碼和人生第一個6000字的word報告。真是好久沒有這么認真的完成作業了。對於本次設計的不足之處,我想應該是實用性不大,或許根本沒有實用的可能性,因為這根本不能稱得上是圖書管理系統,與真正的圖書管理系統相比,我這算是垃圾了吧簡直丑陋至極。不過我很興奮,這算是我大學里第一次起步吧,這學期的數據結構這門課程,我收獲到的不僅是知識,還有很多其他寶貴的東西……就這樣吧也算是對這門課程有了交代。時間還早,路還很長,我還年輕。