数据结构
数据结构三要素——逻辑结构,物理(存储)结构,数据运算
<存储结构不同,运算的实现方式不同>
线性表
线性表的定义2.2.1
—数据结构三要素之逻辑结构
定义(“逻辑结构”)和基本操作 (“运算”)
线性表是具有相同的数据类型的n(n>=0)个数据元素的有限 序列,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为: L=(a1,a2,…,ai,ai+1,…,an) 角标从1开始
元素所占的数据空间是一样大的 有限序列 有序序列
ai是线性表中的“第i个”元素,线性表中的位序(从1开始):用数组实现时需要注意
a1是表头元素;an是表尾元素。
除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
线性表的基本操作 “运算”
带有&的参数:说明有修改需要带回
函数内是形参,不加引用符号修改不了(C++)
InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。 从无到有,
从有到无。
ListInsert(&L,i,e):插入操作。在表L(线性表)中的第i个位置上插入指定元素e。
(函数名(线性表,位置,指定元素的值))
ListDelete(&L,i,&e):删除操作。删除表L中的第i个位置的元素,并用e返回删除元素的值。
(函数名(线性表,位置,指定元素的值)) 增删
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
(可以给定一个元素e的值在线性表在L中查找,线性表中有没有一个数据元素和e的值相同)
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
(传入一个参数i,i指明了想要查找的线性表中的第i个元素) 改、查(”改“之前也要“查”)
其他常用操作:
Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
Empty(L):判空操作。若L为空表,则返回true,否则返回false。
Tips:
- 对数据的操作(记忆思路)——创建,销毁,增删改查
- C语言函数的定义—— <返回值类型> 函数名(<参数1类型> 参数1,<参数2类型> 参数2,…)
- 实际开发中,可根据实际需求定义其他的基本操作
- 函数名和参数的形式、命名都可改变。(key:命名要有可读性)
- 什么时候要传入参数的引用“&”——对参数的修改结果需要“带回来”
为什么要实现对数据结构的基本操作?
- 团队合作编程,你定义的数据结构要让0别人能够很方便的使用(封装)
- 将常用的操作/运算封装成函数,避免重复工作,降低出错风险
顺序表
顺序表的定义2.2.2
顺序表——用顺序存储的方式实现线性表顺序结构。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
线性表L逻辑结构 a1-a2-a3-4-a5
线性表是具有相同数据类型的n(n>=0)个数据元素的有限 序列(每个数据元素所占空间一样大)
内存 |
---|
LOC(L) 设线性表第一个元素的存放位置是LOC(L) LOC是location的缩写 |
a1 存放位置=LOC(L)+数据元素的大小 |
a2 存放位置=LOC(L)+2*数据元素的大小 |
a3 存放位置=LOC(L)+3*数据元素的大小 |
a4 |
a5 |
如何知道一个数据元素大小? C语言中sizeof(Elem Type)
ElemType 就是你的顺序表中存放的数据元素类型
eg:sizeof(int) = 4B
eg: typedef struct{
int num;
int people;
}Customer;
sizeof(int) = 4B
sizeof(Customer) = 8B
顺序表的实现——静态分配
#define MaxSize 10 //定义最大长度
typedef struct{
ElemType data[MaxSize]; //用静态的“数组”存放数据元素
//ElemType填写具体数据类型
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式) Sq:sequence --- 顺序,序列
Q:如果“数组”存满了怎么办?
A:可以放弃治疗,顺序表的表长刚开始确定后就无法更改(存储空间是静态的)
如果刚开始就声明一个很大的内存空间,会很浪费。
给各个数据元素分配连续的存储空间,大小为MaxSize*sizeof(ElemType)
eg:
#include <studio.h>
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态“数组”存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
/*
本例中数据原数类型(ElemType)是int
*/
//基本操作——初始化一个顺序表
void InutList(SqList &L){
for(int i=0; i<MaxSize; i++)//可以去掉
L.data[i] = 0; //将所有数据元素设置为默认初始化值
/*
2.执行for 把各个数据元素的值设为默认值(可省略)
*/
L.length=0; //顺序表初始化长度为0
/*
3. 将length的值设为0
*/
}
int main(){
SqList L; //声明一个顺序表
/*
1. 在内存中分配存储顺序表L的空间。包括:MaxSize*sizeof(ElemType)和存储length的空间
*/
InitList(L);//初始化顺序表
//尝试“违规”打印整个data数组 错误
for(int i=0 ; i<MaxSize (i<L.length这种访问也不够好,更好的做法是使用基本操作来访问各个数据元素);i++)
printf("data[%d]=%d\n",i,L.data[i]);//内存中会有遗留的“脏数据”
return 0;
}
顺序表的实现——动态分配
#define InitSize 10 //顺序表的初始长度
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; //顺序表的类型定义(动态分配方式)
Key: 动态申请和释放内存空间
C——malloc、free函数
申请、 释放
L.data = *(ElemType ) malloc (sizeof(ElemType) * InitSize);
申请一整片连续的内存空间,这内存空间有起始地址,在malloc执行结束后会返回一个指向这一片内存空间开始地址的指针 (malloc 函数返回一个指针,需要强制转型为你定义**(ElemType *)**的数据元素类型指针) InitSize是数组的初始长度
C++ ——new、delete关键字
#include <stdlib.h> //malloc、free函数的头文件
#define IntSize 10 //默认的最大长度
typedef struct{
int *data; //指示动态分配数组的指针
int MaxSize; //顺序表的组大容量
int length; //顺序表的当前长度
}SeqList;
//函数 InitList 用于初始化一个动态分配的顺序表
void InitList(SeqList &L){
//用malloc函数申请一片连续的存储空间
L.data = (int *)malloc(InitSize*sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
}
//增加动态数组的长度
void IncreaseSize(Seqlist &L,int len){
int *p = L.data;
L.data=(int *)mallloc((L.MaxSize+len)*sizeof(int));
for(int i=0;i<L.length; i++){
L.data[i] = p[i]; //将数据复制到新区域 时间开销大!
}
L.MaxSize = L.MaxSize+len; //顺序表最大长度增加 len
free(p); //释放原来的内存空间
}
/*
注:realloc函数也可实现IncreaseSize函数,但建议初学者使用malloc和free更能理解背后的过程
*/
int main(){
SeqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//...往顺序表中随便插入几个元素...
IncreaseSize(L,5);
return 0;
}
顺序表的特点
- 随机访问,即可以在O(1)时间内找到第i个元素。
- 存储密度高,每个结点只存储数据元素 本身 (链式存储,需要在存储元素本身时存储元素位置指针)
- 拓展容量不方便(即便采用动态分配的方式实现,拓展超长度的时间复杂度也比较高)
顺序表的插入和删除2.2.3
插入:
用存储位置的相邻来体现数据元素之间的逻辑关系。
ListInsert(&L,i,e):插入操作。在表L(线性表)中的第i个位置上插入指定元素e。
(函数名(线性表,位置,指定元素的值))
#define MaxSize 10//定义最大长度
typedef struct{
ElemType data[MaxSize];//用静态的"数组"存放数据元素
int length;//顺序表的当前超长度
}SqList;//顺序表的类型定义
//注:该代码简历在顺序表的“静态分配”实现方式之上,“动态分配”也雷同
//1
bool ListInsert(SqList &L,int i ,int e){
if(i<1||i>L.length+1) //判断i的范围是否有效
return false;
if(L.length>=MaxSize) //当前存储空间已满,不能插入
return false;
for(int j=L.length;j>=i;j--) //将第i个元素及之后的元素后移
L.data[j]=L.data[j-1];
L.data[i-1]=e; //在位置i处存放e
L.length++; //长度+1
return true;
}//好的算法,应该具有“健壮性”。能处理异常情况,并给使用者反馈
/*
关注最深层循环语句的执行次数与问题规模n的关系 问题规模 n = L.length 表长
最好情况:新元素插入到表尾,不需要移动元素
i= n+1,循环0次;最好时间复杂度 = O(1)
最坏情况:新元素插入到表头,需要将原有的n个元素全部向后移动
i= 1,循环n次;最坏时间复杂度= O(n);
平均情况:假设新元素插入到任何一个位置的概率相同,即i=1,2,3,...,length+1的概率都是p=1/n+1
i=1,循环n次;i=2,循环n-1次;i=3,循环n-2次...i=n+1时,循环0次
平均循环次数 = np + (n-1)p+(n-2)p+.....+1*p = n(n+1)/2*1/n+1=n/2
所以平均时间为O(n)
*/
//2
void ListInsert(SqList &L,int i ,int e){//i的合法值为 【1,length+1】
for(int j=L.length;j>=i,j--)//将第i个元素及之后的元素后移
L.data[j]=L.data[j-1];
L.data[i-1]=e; //在位置i处放e
L.length++; //长度+1
}
/*
注意位序、数组下标的关系,并从后面的元素依次移动
基操——让自己实现的数据结构可以让别人很方便的使用
*/
int main(){
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//... 此处省略一些代码,插入几个元素
ListInsert(L,3,3);
return 0;
删除:
ListDelete(&L,i,&e):删除操作。删除表L中的第i个位置的元素,并用e返回删除元素的值。
(函数名(线性表,位置,指定元素的值)) 增删
bool ListDelete(SqList &L,int i ,int &e){
if(i<||i>L.length) //判断i的范围是否有效
return false;
for(int j=i;j<L.length;j++) //将被删除的元素赋值给e
L.data[j-1]=L.data[j]; //将第i个位置后的元素前移
L.length--; //线性表长度-1
return true;
}
int main(){
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
int e = -1; //用变量e把删除的元素“带”回来
if(ListDelete(L,3,e))
printf("已删除第3个元素,删除元素值为=%d\n",e);
else
printf("位序i不合法,删除失败\n");
return 0;
}
/*
最好情况:删除表尾元素,不需要移动其他元素
i=n,循环0次;最好时间复杂度=O(1)
最坏情况:删除表头元素,需要将后续的n-1个元素全部向前移动
i=1,循环n-1次;最坏时间复杂度=O(n)
平均情况:假设删除任何一个元素的概率相同,即i=1,2,3,...,length的概率都是p=1/n
i=1,循环n-1次;i=2时,循环n-2次;i=3,循环n-3次...i=n时,循环0次。
平均循环次数=n-1/2 平均时间复杂度=O(n)
*/