输出和输入同样重要,故开启csdn博客之旅
目录
前言
在学习数据结构前,我们要先复习一下语言,本文以c/c++为例回顾顺序表和链表实现的图书管理系统。涉及结构体定义、文件输入输出、顺序表的基本操作、链表的基本操作,需要注意,本文在回顾语言的基础上,重点关心顺序表和链表的基本操作,文件输入输出倒是其次。
程序=算法+数据机构,以知名游戏某神为例,游戏中的角色占用这一程序,即任务A的完成依赖于任务B,任务B的完成依赖于任务C,同时任务C的完成不再可能依赖于任务A,体现的数据结构为有向无环图,算法为拓扑排序,即从一个集合的偏序关系得到其全序关系,通俗而言就是已知任意两者的大小关系而得到全体的大小关系。由此可见,程序中的核心实现依托于典型的数据结构,而学习数据结构离不开算法。
一、数据结构是什么?
数据结构是有结构的数据元素的集合, 描述了这些数据元素之间的关系。
1.基本名词
以学生信息表为例,解释数据、数据项、数据元素、数据对象、数据结构
| 姓名 | 学号 | 班级 |
| 张三 | xxxxx | 计科22-1 |
| 李四 | xxxxx | 计科22-2 |
- 数据:几个名词中最大的概念,即学生信息表的层级
- 数据项:最小单位,学生信息表中的字段,如姓名、学号、班级都是数据项
- 数据元素:基本单位,表中的每一行记录,上表中张三、xxxxxx、计科22-1组成了一条数据元素
- 数据对象:数据的子集,由若干个数据元素组成
- 数据结构:强调数据元素之间的关系,是由相互之间存在特定关系的数据元素构成的集合,如上表的一对一关系
2.性质
数据结构可以分为逻辑结构和存储结构(物理结构),相同的逻辑结构可以有不同的物理实现方式,即不同的存储结构(如线性表的顺序表实现和链表实现),具有相同逻辑结构和存储结构的未必是同一种数据结构,如栈和队列,区分它们的是操作
逻辑结构
根据数据元素之间的关系,可分为普通集合、一对一的线性关系、一对多的树、多对多的图,后面两个统称为非线性结构

存储结构
存储结构分为顺序存储和链式存储,前者常用数组实现,后者使用指针,顺序存储要求逻辑相邻、物理相邻,即通过简单的存储位置关系就能得到某数据元素的后继或前驱,链式存储则无需开辟一段连续的存储空间,缺点是数据元素内部会有一个字段,指针式保存其他数据元素的地址,存储密度低。
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或该链表的长度),首元节点,除头节点和尾节点之外,每个节点都有对应的前驱和后继
- 链表的很多算法题都是衍生于创建链表的前插法和后插法,比如合并有序链表、逆序、逆序合并有序链表等等
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;
}
}
}
总结
链式存储中,插入、删除较为方便,对两者循环的不同边界要加以理解和记忆,关键在于单链表的插入或删除我们总是要找到其前驱节点,而对于删除我们要保证在找到前驱节点的同时待删除节点不为空。排序仅考虑数据域,逆序仅考虑指针域。
顺序表的实现,按索引查找较为方便,插入删除操作量大,存储密度高,不再赘述。
2078

被折叠的 条评论
为什么被折叠?



