数据结构先修之c语言大作业-图书管理系统

输出和输入同样重要,故开启csdn博客之旅

前言

在学习数据结构前,我们要先复习一下语言,本文以c/c++为例回顾顺序表和链表实现的图书管理系统。涉及结构体定义、文件输入输出、顺序表的基本操作、链表的基本操作,需要注意,本文在回顾语言的基础上,重点关心顺序表和链表的基本操作,文件输入输出倒是其次。

程序=算法+数据机构,以知名游戏某神为例,游戏中的角色占用这一程序,即任务A的完成依赖于任务B,任务B的完成依赖于任务C,同时任务C的完成不再可能依赖于任务A,体现的数据结构为有向无环图,算法为拓扑排序,即从一个集合的偏序关系得到其全序关系,通俗而言就是已知任意两者的大小关系而得到全体的大小关系。由此可见,程序中的核心实现依托于典型的数据结构,而学习数据结构离不开算法。


一、数据结构是什么?

数据结构是有结构的数据元素的集合, 描述了这些数据元素之间的关系

1.基本名词

以学生信息表为例,解释数据、数据项、数据元素、数据对象、数据结构

学生信息表
姓名学号班级
张三xxxxx计科22-1
李四xxxxx计科22-2
  • 数据:几个名词中最大的概念,即学生信息表的层级
  • 数据项:最小单位,学生信息表中的字段,如姓名、学号、班级都是数据项
  • 数据元素:基本单位,表中的每一行记录,上表中张三、xxxxxx、计科22-1组成了一条数据元素
  • 数据对象:数据的子集,由若干个数据元素组成
  • 数据结构:强调数据元素之间的关系,是由相互之间存在特定关系的数据元素构成的集合,如上表的一对一关系

2.性质

数据结构可以分为逻辑结构存储结构(物理结构),相同的逻辑结构可以有不同的物理实现方式,即不同的存储结构(如线性表的顺序表实现和链表实现),具有相同逻辑结构和存储结构的未必是同一种数据结构,如栈和队列,区分它们的是操作

逻辑结构

        根据数据元素之间的关系,可分为普通集合、一对一的线性关系、一对多的树、多对多的图,后面两个统称为非线性结构

91c699a93ae64d879e5b4dac3b780982.png

存储结构

        存储结构分为顺序存储链式存储,前者常用数组实现,后者使用指针,顺序存储要求逻辑相邻、物理相邻,即通过简单的存储位置关系就能得到某数据元素的后继或前驱,链式存储则无需开辟一段连续的存储空间,缺点是数据元素内部会有一个字段,指针式保存其他数据元素的地址,存储密度低。

3.分类

线性表、栈、队列、串、树、图

二、顺序表实现图书馆管理系统

1.结构体定义图书、图书数组

#include <bits/stdc++.h>
using namespace std;

typedef struct{
	char no[20];   								//书号
	char name[100]; 							//书名
	float price;   								//价格
}Book;

typedef struct{
	Book *elem; 	//指向数组的指针,leetcode中常有int *nums,这里将Book *elem,理解成book类型的数组即可
	int length; 								//数组的长度
}SqList;                                        //typedef 创建别名,增强可读性

2.动态内存分配、内存释放

#define MAXN 1024
void InitList(SqList &L){   							
//使用动态内存分配new进行初始化
    L.elem = new Book[MAXN];                       //数组最大容量
    L.length = 0;                                  //初始化数组长度
}

void FreeList(SqList &L){
//释放内存
    delete[] L.elem;
    //避免悬挂指针
    L.elem = nullptr;
}

3.从文件中读取图书信息

void ReadFile(SqList &L, string filepath){
//从文件中读取图书信息,将其按顺序存入L.elem指向的数组中
     ifstream file(filepath);
     if(!file.is_open()){
         exit(1);
     }

     //跳过前两行标题信息
     string line;
     getline(file, line); //读取第一行信息
     getline(file, line); // 读取第二行信息

     
     int count = 0;       //计数器
     while( file >> L.elem[count].no >> L.elem[count].name >> L.elem[count].price){
         count++;
     }

     L.length = count;
     file.close();
}

4.保存图书信息到文件中

void SaveFile(SqList &L, string filepath) {        
//保存图书信息到文件

    //(1)
    ifstream fin(filepath);
    if(!fin.is_open()){
        exit(1);
    }
    string line1,line2;
    getline(fin,line1);
    getline(fin,line2);

    fin.close();
    
    //(2)
    ofstream fout(filepath);
    if(!fout.is_open()){
        exit(1);
    }
    fout << line1 <<endl;
    fout << line2 <<endl;

    //(3)
    int i=0;
    while(i<L.length){
        fout<< L.elem[i].no<<'\t'<<L.elem[i].name<<'\t'<<L.elem[i].price<<endl;
        i++;
    }
    fout.close();
}

(1)读取文件中前两行的标题信息

(2)将前两行标题信息写入到文件中

(3)将图书信息写入到文件中

注意,ofstream在向文件中输入的时候,会从头开始,也就是先清空,再写入,所以我们需要先保存前两行的标题信息。

5.插入、删除图书记录

插入

bool InsertBook(SqList &L, int pos, Book *book){
//插入图书信息,输入图书的书号、书名和价格,将该图书的基本信息插入到数组中的指定位置
//如果插入成功,返回true,否则,返回false

    //(1)
    if(pos < 1 || pos>L.length+1 || L.length==MAXN){
        return false;
    }

    //(2)
    for(int i=L.length;i>=pos;i--){
        L.elem[i] = L.elem[i-1];
    }

    //(3)
    L.elem[pos-1].price = book->price;
    for(int i=0;i<20;i++){
        L.elem[pos-1].no[i] = book->no[i];
    }
    for(int i=0;i<100;i++){
        L.elem[pos-1].name[i] = book->name[i];
    }
    
    //(3)
    L.length++;
    return true;
}

(1)检查插入位置是否合法以及数组长度是否达到最大容量

(2)从尾巴开始,到要插入的位置,依次将其向后移动一位

(3)在插入的位置更新图书信息

(4)更新数组长度

时间复杂度=》O(n)

删除

bool DeleteBook(SqList &L, int pos) { 				
//删除指定图书信息
//如果删除成功,返回true,否则,返回false

    //(1)
    if(pos<1 || pos > L.length){
        return false;
    }
    
    //(2)
    for(int i=pos;i<L.length;i++){
        L.elem[i-1] = L.elem[i];
    }
    
    //(3)
    L.length--;
    return true;
}

(1)检查要删除的位置是否合法

(2)从要删除的位置的后驱开始,到尾巴,将其依次向前移动一个单位

(3)更新数组长度

6.查询、更新图书信息

查询

SqList SearchBook(SqList &L, int type) {
//图书信息查找,返回包含查找图书信息的数组和数组的长度,如果不存在,则令长度为0
    
    //(1)
    SqList ret;
    InitList(ret);

    //(2)
        //时间复杂度为o(1)
	if(type == 1){
		int pos;
		cin >> pos;
        if(pos<1||pos>L.length){
            ret.length=0;
        }else{
            ret.elem[0] = L.elem[pos-1];
            ret.length = 1;
        }
        
	}

    //时间复杂度为o(L.length)
	if(type == 2){
		char name[100];
		cin >> name;
        int cnt=0;
        int k =L.length;

        //(3)
        for(int i=0;i<k;i++){
            if(strcmp(L.elem[i].name,name)==0){   //比较两个字符数组是否相同!!!!!!!!!
                ret.elem[cnt] = L.elem[i];
                cnt++;
            }
        }
        //(4)
        ret.length=cnt;
	}
    return ret;
}

(1)创建返回值图书数组并初始化

(2)按位置查找

(3)按值查找

(4)更新数组长度

更新

void UpdateBook(SqList &L) { 
//图书信息更新
    for(int i=0;i<L.length;i++){
        if(L.elem[i].price<45){
            L.elem[i].price = L.elem[i].price*1.2;
        }else{
           L.elem[i].price = L.elem[i].price*1.1;
        }
    }
}

7.图书信息排序(冒泡法)

void PriceSort(SqList &L){
//按图书价格升序排序,采用冒泡排序 
    //(1)
	for(int i=0;i<L.length-1;i++){
        //(2)
        for(int j=0;j<L.length-1-i;j++){
            if(L.elem[j].price>L.elem[j+1].price){
                Book tmp;
                tmp = L.elem[j];
                L.elem[j] = L.elem[j+1];
                L.elem[j+1] = tmp;
            }
        }
    }
}

(1)走L.length-1次,每次将最大的放到最后

(2)每次从头开始,将最大的放在L.length-1-i

8.将图书信息逆序存储

void ReverseBook(SqList &L) {
//将图书信息逆序存储
    for (int i = 0; i < L.length / 2; i++) {
        int temp = L.elem[i];
        L.elem[i] = L.elem[L.length - i - 1];
        L.elem[L.length - i - 1] = temp;
    }
}

三、链表实现图书管理系统

  1. 顺序表在插入删除中要进行大量操作,基于前人的智慧,出现了链表
  2. 链表由节点组成,根据每个节点指针域的个数可分为单链表、双链表,按链接方式又可分为循环链表等,本文实现的图书管理系统基于单链表。
  3. 对于单链表而言,存储是任意的(连续或不连续),而对于链表中的每个节点,内部是连续存储的,分为数据域指针域,由于指针域的存在,造成存储密度低。
  4. 唯一确定一个链表只需要确定它的头指针。为了便于操作,对于一个链表,我们有头指针(引用头节点),头节点(数据域无意义,通常为-1或该链表的长度),首元节点,除头节点和尾节点之外,每个节点都有对应的前驱和后继
  5. 链表的很多算法题都是衍生于创建链表的前插法后插法,比如合并有序链表逆序逆序合并有序链表等等

1.链表节点结构体定义

#include <bits/stdc++.h>
using namespace std;

typedef struct{
	char no[20];					//书号 
	char name[100];					//书名 
	float price;					//价格 
}Book;

typedef struct LNode{
    //(1)
    Book data ;
    //(2)                      //图书信息
    struct LNode *next;             //指向下一结点
}LNode, *LinkList;                 //(3)

(1)数据域,存放图书对象(结构体LNode创建时,就会自动创建一个Book结构体)

(2)指针域,存放下一节点的地址(用p->next来访问下一节点)

(3)LNode,*LinkLIst : 实际相同,都指代LNode一个节点,后者是指针,习惯上用前者随即创建一个节点,后者用来表示一个链表的头指针

LNode *p = new LNode;        //创建一个节点,用p来引用它

LinkList l = new LNode;      //为头指针L创建一个头节点

2.动态内存分配,内存释放


void InitList(LinkList &L){
//使用动态内存分配new进行初始化 
    L = new LNode;
    L->next = NULL;
}

void FreeList(LinkList &L){
//释放内存
    LNode *p = L->next;         //p获取首元节点
    while(p){
        LNode *tmp = p;         //tmp获取当前要删除的节点
        p = p->next;            //更新要删除的节点
        delete tmp;             //删除节点
    }
}

3.从文件中读取图书信息,利用尾插法构建链表

void ReadFile(LinkList &L, string filepath){
//从文件中读取图书信息,利用尾插法构建链表
    ifstream file(filepath);
    if(!file.is_open()){
        exit(1);
    }

    //跳过前两行标题
    string line;
    getline(file, line);
    getline(file, line);

    //(1)
    LNode *r = L;                   //记录尾节点
    //(2)
    LNode *tmp = new LNode;
    while( file >> tmp->data.no >> tmp->data.name >> tmp->data.price){
    
        //(3)  
        tmp->next = NULL;
    
        r->next = tmp;               //将新节点添加到链表中
        r = tmp;                     //更新尾节点
		//(4)
        tmp = new LNode;                               
    }
    file.close();
}

(1)创建一个LNode类型的指针, 用来指向链表的尾节点

(2)创建一个指针对象,tmp引用该对象,并输入该对象的数据域

(3)将当前节点对象的指针与置空,将新节点添加到尾节点的指针域,更新尾节点

(4)tmp引用一个新的节点对象,回到while循环输入数据处

4.计算链表长度,返回图书数量

int CountBook(LinkList &L){
//返回图书总数

    //(1)
    if(!L->next)return 0;

    //(2)
    LNode *p = L->next;

    //(3)
    int j=1;
    while(p->next){
        p=p->next;
        j++;
    }
    return j;
}

(1)判断首元节点是否为空

(2)若获取首元节点

(3)初始化计数器,循环计数,等价于

int j = 0;
while( p ){
    j++;
    p = p->next;
}

(4)链表的下一节点为空,返回计数器大小

4.保存图书信息到文件

void SaveFile(LinkList &L, string filepath) {
//保存图书信息到文件
    ofstream file(filepath);
    if(!file.is_open()){
        exit(1);
    }

    //(1)
    file <<"北京林业大学图书馆计算机类图书采购列表"<<endl;
    file <<"ISBN	                  书名	                定价"<<endl;

    //(2)
    LNode *p = L->next;        //首元节点
    while(p){
        file << p->data.no <<'\t'<< p->data.name <<'\t'<< p->data.price << endl;
        p = p->next;
    }
    file.close();
}

(1)输出标题信息到文件中

(2)创建一个节点指针,初始化指向首元节点,当该节点不为空的时候,循环输出图书信息到文件中去

5.插入 删除 图书信息

删除


bool DeleteBook(LinkList &L, int pos) { 				
//删除指定图书信息
//如果删除成功,返回true,否则,返回false

    //(1)
    LNode * p =L;
    int j = 0;
    while(p->next && j<pos-1){
        p = p->next;
        j++;
    }                                       //来到要删除的节点的前驱
    //(2)
    if(!(p->next) || j>pos-1){              //如果要删除的节点为空或pos<1
        return false;
    }
    //(3)
    LNode *tmp = p->next;                   //保存要删除的节点

    //(4)
    p->next = tmp->next;                      //逻辑删除
    delete tmp;                             //物理删除
    return true;
}

(1)获取要删除的节点的前驱,注意while循环的条件是p->next不为空,若其为空,即使来到了前驱节点,删除操作也是错误的

(2)我们期望while循环的退出条件是j==pos-1 && p->next,即我们来到了要删除的前驱节点,但同时我们还希望p->next不为空,所以要加一个特判,判断循环结束条件是否为j==pos-1(我们来到了要删除的节点的前驱节点)

(3)保存一下要删除的节点,然后先从逻辑上删除,再从内存中删除

(4)注意是p->next = tmp->next ,第一次做的时候写成了p = tmp->next

插入


bool InsertBook(LinkList &L, int pos, Book *book){
//插入图书信息,输入图书的书号、书名和价格,将该图书的基本信息插入到链表中的指定位置
//如果插入成功,返回true,否则,返回false

    //(1)
    LNode* p =L;
    int j=0;
    while(p && j<pos-1){
        p = p->next;
        j++;
    }
    //(2)
    if(!p || j>pos-1){
        return false;
    }
    //(3)
    LNode * tmp = new LNode;
    tmp->data = *book;
    tmp->next = p->next;
    p->next = tmp;
    return true;
}

(1)获取要插入位置的前驱节点

(2)我们希望来到插入位置的前驱节点,所以该节点不能为空,同时判断一下插入位置是否合法(pos是否小于1)

(3)创建新节点,保存数据域信息,更新其指针域为插入位置前驱节点的后继,再更新前驱节点的后继

6.图书信息查找

LinkList SearchBook(LinkList &L, int type) {
//图书信息查找,返回包含查找图书信息的链表,如果查找失败,返回nullptr

    //(1)    
    LinkList ret;
    InitList(ret);


    //(2)
	if(type == 1){
		int pos;
		cin >> pos;
        int j = 0;
        LNode *p = L;
        //(3)
        while(p && j<pos){
            p=p->next;
            j++;
        }

        //(4)
        if(!p || j>pos){
            return nullptr;
        }
        
        //(5)
        LNode *tmp = new LNode;
        tmp->data = p->data;
        ret->next =tmp;
        return ret;
	}
    //(6)
    LNode *p_ret = ret;
	if(type == 2){
		char name[50];
		cin >> name;
        LNode * p =L->next;
        LNode *tmp;
        while(p){
            if(strcmp(p->data.name,name)==0){
                tmp = new LNode;
                tmp->data = p->data;
                p_ret->next = tmp;
                p_ret = p_ret->next;
            }
            //(7)
            p = p->next;
        }
        return ret; 
	}
}

(1)创建返回链表并初始化

(2)处理位置查找

(3)while(p&&p<pos)我们希望循环结束时来到第pos个节点,区别于插入或删除寻找的pos-1,我们这里并不需要向删除或插入一样找到被操作数的前驱

(4)查询位置不合法,返回空指针

(5)找到欲查询节点,将其添加到返回链表中

(6)处理值查找

        1.定义一个指针p_ret,复制一下返回值ret(因为后续如果查询到的数据记录较多的话要对p_ret作递增处理)

        2.输入欲查询的值,更新p为查询范围的首元节点

        3.如果找到该值对应的节点就更新到返回链表中

7.图书信息更新

void UpdateBook(LinkList &L) {
//图书信息更新
    LNode *p = L->next;
    while(p){
        if(p->data.price<45){
            p->data.price *=1.2;
        }else{
            p->data.price *=1.1;
        }
        p = p->next;
    }
}

8.逆序存储

void ReverseBook(LinkList &L){
//把图书信息逆序存储 
//第一想法是直接交换对应位置的节点的数据域,这样的话时间复杂度会很高,对于每一个节点都需要去找到相反位置
//开辟一个新的链表也没啥用,毕竟对于一个节点我们只能访问其后继
//考虑操作指针
    //(1)
    LNode *pre =NULL;
    LNode *cur = L;
    LNode *next;
    //(2)
    while(cur!=NULL){
        next  = cur->next;  //对于最后一个节点,next为null 
        cur->next = pre;	//cur->next 为倒数第二个节点 
        pre = cur;          //pre为最后一个节点-首元节点 
        cur = next;			//cur为null 
    }
    //(3)
	LinkList list;
	InitList(list);
	list->next = pre;  
    L = list;
}

对于链表的逆序存储,不再可以像顺序表那样直接利用索引方便的进行

我们考虑交换指针

一般的,对于每个节点,我们记录cur,pre,next,当前节点、前驱、后继

当cur不为空的时候,

        更新next为cur->next

        更新cur-->next 为pre //实现了逆序

        更新pre为cur

        更新cur为next

最后我们希望cur为空,pre为首元节点,那么只需要再创建一个表头,其头节点的指针指向pre

(1)记录当前节点、前驱、后继

(2)循环更新每个节点的指针域

(3)创建表头

除了这种迭代的方式外,我们还可以采用前插法直接交换节点

使用传入的头指针L,定义一个工作指针p指向首元节点,临时指针q

头指针指针域置为NULL(必要)

临时指针q保存工作指针p的下一节点

更新工作指针p的下一节点为头节点的下一节点

更新头节点的下一节点为工作指针p所指节点

更新工作指针p为q

void ReverseBook(LinkList &L){
//把图书信息逆序存储
    //(1) 
   LNode *p = L->next;
    //(2)
   LNode *q;
    //(3)
   L->next = NULL;
    //(4)
   while(p){
        q = p->next;
        p->next = L->next;
        L->next = p;
        p = q;
    }
    
}

(1):定义工作指针p为待插入节点

(2):定义临时指针q用来储存工作指针p的下一节点

(3):初始化,将头节点的指针域置空=》这是必要的,因为在链表遍历判断终止的条件就是p!=NULL

(4):前插法插入节点

9.根据数据域排序

void PriceSort(LinkList &L){
//按图书价格升序排序,采用冒泡排序
//一开始的考量是交换两个节点,但这样每次交换就需要记录前后四个节点
//所以只要交换数据域就可以了
	int len = CountBook(L);
    for(int i = 0;i<len-1;i++){
        LNode *p = L->next;
        for(int j=0;j<len-1-i;j++){
            if(p->data.price > p->next->data.price){
               Book tmp = p->data;
               p->data = p->next->data;
               p->next->data = tmp; 
            }
            p = p->next;
        }
    }
}

总结

链式存储中,插入、删除较为方便,对两者循环的不同边界要加以理解和记忆,关键在于单链表的插入或删除我们总是要找到其前驱节点,而对于删除我们要保证在找到前驱节点的同时待删除节点不为空。排序仅考虑数据域,逆序仅考虑指针域。

顺序表的实现,按索引查找较为方便,插入删除操作量大,存储密度高,不再赘述。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值