数据结构引进
0数据结构相关概念
0.1什么是数据结构
数据结构是由"数据"和"结构"两词组合而来。
什么是数据?常见的数值1、2、3、4...、教务系统⾥保存的用户信息(姓名、性别、年龄、学历等等)、网页肉眼可以看到的信息(⽂字、图⽚、视频等等),这些都是数据
什么是结构?
当我们想要使用⼤量使用同⼀类型的数据时,通过手动定义⼤量的独立的变量对于程序来说,可读性非常差,我们可以借助数组这样的数据结构将大量的数据组织在⼀起,结构也可以理解为组织数据的方式。
想要找到草原上名叫"咩咩"的羊很难,但是从羊圈里找到1号羊就很简单,羊圈这样的结构有效将羊群组织起来。
概念:**数据结构是计算机存储、组织数据的方式。**数据结构是指相互之间存在⼀种或多种特定关系的数据元素的集合。数据结构反映数据元素之间呈现的结构。
总结:
(1).能够存储数据(如顺序表,链表等结构)
(2).存储的数据能够方便查找
0.2.为什么需要数据结构
程序如果不对数据进行管理,可能导致数据丢失,操作数据困难期,野指针等情况
通过数据结构,能够有效将数据组织和管理在一起,按照我们的方式任意对数据进行增删改查等操作
最基础的数据结构:数组
假定数组有10个空间,已经使用了5个,向数组中插入数据步骤:
求数组的长度,求数组的有效数据个数,向下标为数据有效个数的位置插入数据(注意:这里是否要判断数组是否满了,满了还能继续插入吗)...
假设数据量非常庞⼤,频繁的获取数组有效数据个数会影响程序执行效率。
结论:最基础的数据结构能够提供的操作已经不能完全满足复杂算法实现。
顺序表
1.顺序表的概念及结构
1.1线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列,线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表,链表,栈,队列,字符串...
线性表在逻辑上是线性结构,也就是连续的一条直线,但是物理结构上并不一定是连续的
线性表在物理上存储时,通常以数组和链式结构形式存储
线性表指的是具有部分相同特性的一类数据结构的集合
2.顺序表分类
顺序表和数组的区别
- 顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接口
顺序表分类 - 静态顺序表
- 概念:使用定长数组存储元素
+静态顺序表缺陷:空间给少了不够用,给多了造成空间浪费
- 概念:使用定长数组存储元素
动态顺序表
3 .动态顺序表的实习现
c
#define INIT_CAPACITY 4
typedef int SLDataType;
// 动态顺序表 -- 按需申请
typedef struct SeqList
{
SLDataType* a;
int size; // 有效数据个数
int capacity; // 空间容量
}SL;
//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);
//扩容
void SLCheckCapacity(SL* ps);
//头部插⼊删除 / 尾部插⼊删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
//指定位置之前插⼊/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
int SLFind(SL* ps, SLDataType x);
顺序表的应用
4.基于动态顺序表实现通讯录
- C语言基础要求:结构体、动态内存管理、顺序表、文件操作
4.1功能要求
(1)至少能够存储100个人的通讯信息
(2)能够保存用户信息:名字,性别,年两千,电话,地址等
(3)增加联系人信息
(4)删除指定联系人
(5)查找指定联系人
(6)修改指定联系人
(7)显示联系人信息
4.2代码实现
【思考1】⽤静态顺序表和动态顺序表分别如何实现
【思考2】如何保证程序结束后,历史通讯录信息不会丢失
c
//SeqList.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include"contact.h"
//数据类型为PersonInfo
typedef struct PersonInfo SQDataType;
//typedef int SQDataType;
//动态顺序表
typedef struct SeqList {
SQDataType* a;
int size;//保存有效数据个数
int capacity;//空间的⼤⼩
}SLT;
//初始化与销毁
void SeqListInit(SLT* psl);
void SeqListDesTroy(SLT* psl);
void SeqListPrint(SLT sl);
void CheckCapacity(SLT* psl);
// 头部插⼊删除 / 尾部插⼊删除
void SeqListPushBack(SLT* psl, SQDataType x);
void SeqListPushFront(SLT* psl, SQDataType x);
void SeqListPopBack(SLT* psl);
void SeqListPopFront(SLT* psl);
//查找
int SeqListFind(SLT* psl, SQDataType x);
// 在指定位置之前插⼊/删除
//void SeqListInsert(SLT* psl, int pos, SQDataType x);
void SeqListInsert(SLT* psl, size_t pos, SQDataType x);
void SeqListErase(SLT* psl, size_t pos);
size_t SeqListSize(SLT* psl);
//修改指定位置的值
void SeqListAt(SLT* psl, size_t pos, SQDataType x);
#include<stdlib.h>
#include"contact.h"
//数据类型为PersonInfo
typedef struct PersonInfo SQDataType;
//typedef int SQDataType;
//动态顺序表
typedef struct SeqList {
SQDataType* a;
int size;//保存有效数据个数
int capacity;//空间的⼤⼩
}SLT;
//初始化与销毁
void SeqListInit(SLT* psl);
void SeqListDesTroy(SLT* psl);
void SeqListPrint(SLT sl);
void CheckCapacity(SLT* psl);
// 头部插⼊删除 / 尾部插⼊删除
void SeqListPushBack(SLT* psl, SQDataType x);
void SeqListPushFront(SLT* psl, SQDataType x);
void SeqListPopBack(SLT* psl);
void SeqListPopFront(SLT* psl);
//查找
int SeqListFind(SLT* psl, SQDataType x);
// 在指定位置之前插⼊/删除
//void SeqListInsert(SLT* psl, int pos, SQDataType x);
void SeqListInsert(SLT* psl, size_t pos, SQDataType x);
void SeqListErase(SLT* psl, size_t pos);
size_t SeqListSize(SLT* psl);
//修改指定位置的值
void SeqListAt(SLT* psl, size_t pos, SQDataType x);
typedef struct SeqList contact;
//⽤⼾数据
typedef struct PersonInfo
{
char name[NAME_MAX];
char sex[SEX_MAX];
int age;
char tel[TEL_MAX];
char addr[ADDR_MAX];
}PeoInfo;
//初始化通讯录
void InitContact(contact* con);
//添加通讯录数据
void AddContact(contact* con);
//删除通讯录数据
void DelContact(contact* con);
//展⽰通讯录数据
void ShowContact(contact* con);
//查找通讯录数据
void FindContact(contact* con);
//修改通讯录数据
void ModifyContact(contact* con);
//销毁通讯录数据
void DestroyContact(contact* con);
//contact.c
#define _CRT_SECURE_NO_WARNINGS
#include"contact.h"
#include"SeqList.h"
void LoadContact(contact* con) {
FILE* pf = fopen("contact.txt", "rb");
if (pf == NULL) {
perror("fopen error!\n");
return;
}
//循环读取⽂件数据
PeoInfo info;
while (fread(&info,sizeof(PeoInfo),1,pf))
{
SeqListPushBack(con, info);
}
printf("历史数据导⼊通讯录成功!\n");
}
void InitContact(contact* con) {
SeqListInit(con);
LoadContact(con);
}
void AddContact(contact* con) {
PeoInfo info;
printf("请输⼊姓名:\n");
scanf("%s", &info.name);
printf("请输⼊性别:\n");
scanf("%s", &info.sex);
printf("请输⼊年龄:\n");
scanf("%d", &info.age);
printf("请输⼊联系电话:\n");
scanf("%s", &info.tel);
printf("请输⼊地址:\n");
scanf("%s", &info.addr);
SeqListPushBack(con, info);
printf("插⼊成功!\n");
}
int FindByName(contact* con, char name[]) {
for (int i = 0; i < con->size; i++)
{
if (0 == strcmp(con->a[i].name, name)) {
return i;
}
}
return -1;
}
void DelContact(contact* con){
char name[NAME_MAX];
printf("请输⼊要删除的⽤⼾姓名:\n");
scanf("%s", name);
int pos = FindByName(con, name);
if (pos < 0) {
printf("要删除的⽤⼾不存在,删除失败!\n");
return;
}
SeqListErase(con, pos);
printf("删除成功!\n");
}
void ShowContact(contact* con){
printf("%-10s %-4s %-4s %15s %-20s\n", "姓名", "性别", "年龄", "联系电话",
for (int i = 0; i < con->size; i++)
{
printf("%-10s %-4s %-4d %15s %-20s\n",
con->a[i].name,
con->a[i].sex,
con->a[i].age,
con->a[i].tel,
con->a[i].addr);
}
}
void FindContact(contact* con){
char name[NAME_MAX];
printf("请输⼊要查找的⽤⼾姓名:\n");
scanf("%s", name);
int pos = FindByName(con, name);
if (pos < 0) {
printf("要查找的⽤⼾不存在,查找失败!\n");
return;
}
printf("查找成功!\n");
printf("%-10s %-4s %-4d %15s %-20s\n",
con->a[pos].name,
con->a[pos].sex,
con->a[pos].age,
con->a[pos].tel,
con->a[pos].addr);
}
void ModifyContact(contact* con) {
char name[NAME_MAX];
printf("请输⼊要修改的⽤⼾名称:\n");
scanf("%s", name);
int pos = FindByName(con, name);
if (pos < 0) {
printf("要查找的⽤⼾不存在,修改失败!\n");
return;
}
PeoInfo info;
printf("请输⼊要修改的姓名:\n");
scanf("%s", &con->a[pos].name);
printf("请输⼊要修改的性别:\n");
scanf("%s", &con->a[pos].sex);
printf("请输⼊要修改的年龄:\n");
scanf("%d", &con->a[pos].age);
printf("请输⼊要修改的联系电话:\n");
scanf("%s", &con->a[pos].tel);
printf("请输⼊要修改的地址:\n");
scanf("%s", &con->a[pos].addr);
printf("修改成功!\n");
}
void SaveContact(contact* con) {
FILE* pf = fopen("contact.txt", "wb");
if (pf == NULL) {
perror("fopen error!\n");
return;
}
//将通讯录数据写⼊⽂件
for (int i = 0; i < con->size; i++)
{
fwrite(con->a + i, sizeof(PeoInfo), 1, pf);
}
printf("通讯录数据保存成功!\n");
}
void DestroyContact(contact* con) {
SaveContact(con);
SeqListDesTroy(con);
}
//test.c
#include"SeqList.h"
#include"contact.h"
void menu() {
//通讯录初始化
contact con;
InitContact(&con);
int op = -1;
do {
printf("********************************\n");
printf("*****1、添加⽤⼾ 2、删除⽤⼾*****\n");
printf("*****3、查找⽤⼾ 4、修改⽤⼾*****\n");
printf("*****5、展⽰⽤⼾ 0、退出 *****\n");
printf("********************************\n");
printf("请选择您的操作:\n");
scanf("%d", &op);
switch (op)
{
case 1:
AddContact(&con);
break;
case 2:
DelContact(&con);
break;
case 3:
FindContact(&con);
break;
case 4:
ModifyContact(&con);
break;
case 5:
ShowContact(&con);
break;
default:
printf("输⼊有误,请重新输⼊\n");
break;
}
} while (op!=0);
//销毁通讯录
DestroyContact(&con);
}
5.顺序表经典算法
经典算法OJ题1:移除元素
经典算法OJ题2:合并两个有序数组
6.顺序表的问题及思考
-
1.中间/头部的插入删除,时间复杂度为O(n)
- 解决:
- 使用链表结构:对于单链表,在头部插入节点只需要重新调整指针,时间复杂度为O(1)。插入操作只需创建一个新节点,将新节点的指针指向原头部节点,然后更新头部指针指向新节点。在中间插入节点时,先找到要插入位置的前驱节点,然后调整指针将新节点插入,时间复杂度取决于查找前驱节点的时间,平均为O(n),但在已知位置插入时不需要移动大量数据,相比于顺序表性能有提升。
- 使用跳表(Skip List)结构(适用于有序数据):跳表是在链表基础上改进的数据结构。它通过增加多层索引来提高查找、插入和删除的效率。在跳表中,每个节点有多个指针,除了指向后继节点的指针外,还有指向更高层索引节点的指针。查找一个元素时,可以通过高层索引快速定位到目标元素附近,然后在底层链表进行精确查找。插入和删除操作也能利用索引快速定位到相应位置,虽然插入和删除操作需要更新索引,但时间复杂度平均为O(log n),远低于顺序表在中间/头部操作的O(n)。
-
2.增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗
- 解决:
- 预分配策略:根据数据的增长规律,采用更合理的预分配策略。例如,如果能预估数据量的大致范围,可以一次性分配足够的空间,避免频繁的增容操作。如果数据增长较为缓慢,可以按照较小的固定步长进行预分配,而不是简单的2倍增长。
- 内存池技术:建立一个内存池,预先申请一块较大的内存空间。当需要为顺序表分配空间时,从内存池中获取内存块,而不是直接向操作系统申请。当顺序表释放空间时,将内存块归还到内存池而不是直接释放给操作系统。这样可以减少频繁的系统调用(申请和释放内存都涉及系统调用),提高效率。
-
3.增容一般是2倍增长,势必有一定的空间浪费例如当前容量为100,满了以后增容到
200,我们再继续插⼊了5个数据,后⾯没有数据插⼊了,那么就浪费了95个数据空间。
- 解决:
- 动态调整增容步长:不再固定按照2倍增长的方式进行增容。可以根据已使用空间和总空间的比例来动态调整增容步长。例如,当已使用空间达到总空间的80%时,可以按照1.5倍的步长进行增容;当已使用空间较低时,可以采用更小的增容步长。
- 使用紧凑存储结构(如动态数组的优化):在适当的时候对数组进行紧凑操作。例如,当有大量的删除操作后,数组中可能存在很多空闲空间。可以将有效数据重新排列到数组的前部,释放后面的空闲空间。这样在后续的插入操作中,可以利用这些释放的空间,减少增容的需求。