考研408复习笔记—— 数据结构(二)
编写不易,希望各位看到能点个赞。
若发布的内容有什么错误,欢迎留言探讨。
前篇链接
二、线性表(一)
2.1 定义
1、线性表是具有相同数据类型
的n(n>=0)数据元素的有限序列
,其中n为表长
,当n=0时线性表是一个空表
。
基本逻辑结构如下图:
若用L命名线性表,则其一般表示为
【注】
1)由于线性表中的元素具有相同的数据类型
,所以每个数据元素所占用的空间是一样大的,可以方便计算机快速找到元素的位置。
2)线性表是序列,所有元素之间是有次序的。同时,线性表是有限的,像所有整数按次序排列,就不可以说成是线性表。
3)几个概念:
ai是线性表中的“第i个”元素中位序
。位序是从1开始的,数组是从0开始的。
a1是表头元素
,an是表尾元素
。
除了第一个元素外,每个元素都有且仅有一个直接前驱
。
除了最后一个元素外,每个元素有且仅有一个直接后继
。
2.2 基本操作
1、初始化与销毁:
InitList(&L):初始化
线性表,构造一个空的线性表L,分配内存空间。
DestoryList(&L):销毁
线性表,并释放线性表L所占用的内存空间。
2、插入删除:
ListInsert(&L,i,e):插入
操作,在表L中的第i个位置上插入元素e
。
ListDelete(&L,i,&e):删除
操作,删除表L中第i个位置上的元素,并使用e返回删除元素的值。
3、查找:
LocateElem(L,e):按值查找
操作,在表L中查找具有给定值e的元素。
GetElem(L,i):按位查找
操作,获取表L中第i个位置的元素的值。
4、其他常用操作:
Length(L):求表长。返回线性表L的长度,即L中元素的个数。
PrintList(L):输出操作。按前后顺序输出
线性表L的所有元素值。
Empty(L):判空操作。若L为空表,则返回true,否则返回false。
Tips:
1)对数据的基本操作——创建、销毁、增删改查
2)函数的定义——<返回值类型> 函数名(<参数类型1> 参数1,<参数类型2> 参数2,……)
3)实际开发时,可以根据实际重新定义基础操作。
4)上述例子中的L,i,e的变量以及函数的名称参考自,严蔚敏版《数据结构》(大部分学校的指定书目)。
2.3 顺序表
2.3.1 定义
顺序表:用顺序存储
的方式实现线性表。
顺序存储:把逻辑上相邻的
元素存储在物理上也相邻的
存储单元中,元素之间的关系由存储单元的邻接关系
来体现。
如图所示:
该表的在计算机中按顺序存储,就应在内存中相邻。若第一个元素的存放位置为L,则第二个元素存放位置便为L+数据元素的大小1,第三个为L+数据元素的大小2,以此类推,如下图:
Tips:在C语言中使用sizeof(数据元素类型)可以查看一个数据元素的大小。
如
sizeof(int) = 4B;
2.3.2 顺序表的特点
1)随机访问:即可在O(1)时间内找到第i个元素。
2)存储密度高:每个节点只存储数据元素。
3)扩展容量不方便:即便采用动态分配的方式来实现,拓展长度的时间复杂度也较高。
4)插入、删除操作不方便:需要移动大量的元素。
2.3.3 静态分配
静态分配
一般使用数组
来存放数据元素,存放元素的多少是事先固定的,在程序运行的过程中不可更改
。伪代码如下:
#define MaxSize 10 //定义最大长度
typedef struct{
ElemType data[MaxSize]; //用静态的“数组”存放数据元素
int Length; //顺序表当前长度
}SqList; //顺序表的类型定义
在实际代码中,我们需要给ElemType一个确切的数据类型,这样就可以直接在主函数中如int等数据类型一般直接使用SqList来声明变量,并进行操作。
在实际使用中的代码:
#include <iostream>
using namespace std;
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的“数组”存放数据元素
int Length; //顺序表当前长度
}SqList; //顺序表的类型定义
void InitList(SqList &L){
for(int i=0; i<MaxSize;i++){
L.data[i]=0; //将所有的数据元素设置为默认初始值0
}
L.length=0; //顺序表初始长度为0
}
int main(){
SqList L; //创建顺序表实例
InitList(L); //初始化顺序表
return 0;
}
Tips:
如果不给顺序表进行初始化,那么由于在申请顺序表的空间时,所用地址并不全是分配的空地址,因此,顺序表申请到的地址中可能会存在遗留的“脏数据”
,这便需要我们进行一个初始化将内容初始为0。
2.3.4 动态分配
静态分配
一般使用指针
来关联数据元素,存放元素的多少可以依靠实际使用的数量来决定的`。伪代码如下:
#define InitSize 10 //顺序表的初始长度
typedef struct{
ElemType *data; //指针数组实现动态分配
int MaxSize; //顺序表的最大容量
int Length; //顺序表当前长度
}SeqList; //顺序表的类型定义(动态分配)
C中存在malloc函数
用于动态申请内存空间,free函数
用于释放内存空间。
free函数在考研中经常会使用到,建议多了解学习。
C++中则使用指针搭配new
来动态申请空间,释放空间则使用delete
。
实际应用如下:
#incude<stdlib.h> //调用malloc和free的库
#define InitSize 10 //默认的最大长度
typedef struct{
int *data; //指针数组实现动态分配
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SqeList;
void InitSize(SqeList &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 *)malloc((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); //释放原有的内存空间
}
int main(){
SeqList L; //声明一个顺序表
InitList(L); //初始化顺序表
/*
此处需加入插入元素的操作。
*/
IncreaseSize(L,5); //将顺序表扩展5个位置
return 0;
}
相对来说,动态分配再顺序表长度
上更加灵活,但是再扩展过程中需要复制数组的内容,因此耗费的时间
将会增加。
2.3.5 顺序表的插入
ListInsert(&L,i,e):插入
操作,在表L中的第i个位置上插入元素e
。
因为顺序表需要用存储位置的相邻
来体现数据元素之间的逻辑关系,因此在第i个位置插入元素的时候,我们需要把i位之后的所有元素向后面的地址移动。同时,如果是动态数组,我们可能还要考虑一下先扩展表的空间再插入。
代码如下:
#define MaxSize 10
typedef struct{
int data[MaxSize];
int length;
}SqList;
//该代码为静态分配的顺序表中数据未满时插入数据的操作。
void ListInsert(SqList &L,int i,int e){
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); //在第三个元素插入3
return 0;
}
Tips:
在应用实际代码时,需要考虑i是否在表的范围内,同时要考虑数据表是否已将存满,并要给插入失败的操作一个提示。
时间复杂度:
当i位处于表尾,则最快时间复杂度为O(1)。
当i位处于表头时,最坏时间复杂度位O(n)。
相对平均时间:假设插入任何一个元素的该路相同,即i=1,2,3,4,……length+1,的概率都是p=1/(n+1)。
平均循环次数=(n-1)p+(n-2)p+……+p=n/2
因此,插入操作的平均时间复杂度为n/2,所以平均时间复杂度为O(n)。
2.3.6 顺序表的删除
ListDelete(&L,i,&e):删除
操作,删除表L中第i个位置上的元素,并使用e返回删除元素的值。
与插入步骤相似,删除步骤需要我们先定位到i位置,然后删除该位置上的值,再将i位之后的元素挨个前移,并将表长length-1,从而彻底实现删除操作。
代码如下:
bool ListDelete(SqList &L,int i,int &e){
if(i<1||i>L.length){ //判断i位置是否有效
return false; //i位置不合法则返回false
}
e=L.data[i-1]; //将被删除的元素赋值给e
for(int j=i;j<L.length;j++){ //将第i个位置之后的数据前移
L.data[j-1]=L.data[j];
}
L.length-1; //线性表长度-1
return true;
}
int main(){
SqList L;
InitList(L);
int e=-1; //声明一个变量e用于保存被删除的数据元素
/*
省略插入数据元素的步骤
*/
if(ListDelete(L,3,e)){
//输出元素e
} else{
//返回提示i位置不合法。
}
return 0;
}
该处的删除函数使用了bool类型来进行返回,意在以此来查看删除操作是否成功执行。
时间复杂度:
当i位处于表尾,则最快时间复杂度为O(1)。
当i位处于表头时,最坏时间复杂度位O(n)。
相对平均时间:假设删除任何一个元素的概率相同,即i=1,2,3,4,……length,的概率都是p=1/n。
平均循环次数=(n-1)p+(n-2)p+……+p=(n-1)/2
因此,删除操作的平均复杂度为(n-1)/2,所以平均时间复杂度为O(n)。
2.3.7 顺序表的查找
1、按位查找
GetElem(L,i):按位查找
操作,获取表L中第i个位置的元素的值。
相对于按值查找,按位查找的操作相对简单,如数组一般只需通过数据的位置i便可以直接取到相应的数据。代码如下:
ElemType GetElem(SqList L,int i){ //具体函数类型根据数据元素的类型进行更改
return L.data[i-1]; //返回相应位置对应的数据值
}
该代码只是简单的进行了数据的调取,具体的内容需根据实际数据使用情况进行更改。但是无论是静态分配的顺序表还是动态分配的,都可以使用这种按位查找的方法直接调取对应位置的数据。
同样该查找的步骤仅有一步,所以时间复杂度为O(1)。
2、按值查找
LocateElem(L,e):按值查找
操作,在表L中查找具有给定值e的元素。
相对于按位查找,按值查找就相对有些复杂,我们需要从头遍历所有的数据元素,从中找到与所查数据元素相同的数据,并返回它的位置。代码如下:
//在顺序表中查找第一个元素值为e的元素位置
int LocateElem(SqList L,ElemType e){
for(int i=0;i<L.lengrh;i++){ //遍历所有元素
if(L.data[i]==e){ //如果该位数据与e相同
return i+1; //返回该位置的信息,数组是从0开始因此返回数据+1
}
}
return 0; //查找失败,退出循环
}
该查找同样在两种分配方式中都可以直接使用,只需根据具体情况对其中内容进行一定的更改。
相对于按位查找,按值查找所需的时间就比较长了,平局时间复杂度的计算就如插入删除一样,通过概率进行计算之后,平均时间为(n+1)/2,平均时间复杂度为O(n)。
此处的查找只是顺序表的基础查找,