数据结构三要素——逻辑结构、数据的运算、存储结构(物理结构)
小提:储存结构不同,运算的实现方式不同。
从此博客开始到后续的一段时间,都是计蒙本人在为考研做准备。不定时输出用于复习。(加粗字体需要重点理解,每句话后面可能会给出简单解释。)
线性表是具有相同数据类型的n(n≥0)个数据元素的有限 序列,其中n为表长,当n = 0时线性表是一个空表。若用L命名线性表,则其一般表示为:
相同:每个数据元素所占的空间一样大。
序列:有次序。(线性序列关系)
List = (D,R)
其中:D = {ai | ai∈ElemSet,i=1,2,…n,n≥0}
R = {N}, N = {< ai-1 , ai > | ai-1 , ai ∈D , i = 2,3,…n}
特点:在数据元素的非空有限集中
“&” —— 对参数的修改结果需要“带回来”。
#define MaxSize 10 //定义最大长度
typedef struct{
ElemType data[MaxSize]; //用静态的“数组”存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)
在C语言中一般用sizeof(ElemType)来得出一个数据元素的大小
如:sizeof(int) = 4B
所以静态分配的储存空间,大小为
MaxSize*sizeof(ElemType)
#define InitSize 10 //顺序表的初始长度
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
} SeqList; //顺序表的类型定义(动态分配方式)
C:
L.data = (ElemType *) malloc (sizeof(ElemType) * InitSize);
C++:
L.data=new ElemType[InitSize]
void InitList(SeqList &L){
//动态申请一片连续的储存空间
L.data = (ElemType *) malloc (sizeof(ElemType) * InitSize);
L.length=0;
L.MaxSize=InitSize;
}
int main{
SeqList L;
InitList(L);
.....
}
ListInsert(&L,i,e):插入操作。在表L中的**第i个位置上(位序)**插入指定元素e。
编码/考试时建议考虑算法的要求-(健壮性,可读性等)
bool ListInsert(SqList &L, int i, ElemType e)
{
if (i<1 || i>L.length + 1) //判断位置是否有效
{
//位置无效
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++; //长度加一
return true;
}
时间复杂度(平均时间复杂度):O(n) n/2
不需要将删除元素赋值给e
bool DeleteList(SqList &L, int i)
{
if (i<1 || i>L.length)
{
return false;
}
for (int j = i; j <= L.length - 1; j++)//位置i之后元素依次前移覆盖
{
L.data[j - 1] = L.data[j];
}
L.length--; //线性表长度减1
return true;
}
将删除元素赋值给e
bool DeleteList(SqList &L, int i,int &e)
{
if (i<1 || i>L.length)
{
return false;
}
e=L.data[i-1] //数组下标从0开始
for (int j = i; j <= L.length - 1; j++)//位置i之后元素依次前移覆盖
{
L.data[j - 1] = L.data[j];
}
L.length--; //线性表长度减1
return true;
}
时间复杂度(平均时间复杂度):O(n) (n+1)/2
按位查找:获取表L中第i个位置的元素的值
ElemType GetElem(SqList L,int i){
return L.data[i-1];
}
时间复杂度:O(1)
按值查找: 在表L中查找具有给定关键字值的元素
int LocateElem(SeqList L,ElemType e){
for(int i=0;i<L.length;i++)
if(L.data[i]==e){
return i+1; //如果数组下标为i的元素值等于e,返回其位序i+1
}
return 0; //在循环中没找到值,说明查找失败。
}
平均时间复杂度 = O(n) (n+1)/2
注意:基本数据类型:int、char、double、float 等可以直接用运算符“==”比较,而结构体不能直接用。
概念简洁点:
每个结点中只存放数据元素
优点:可随机存取,存储密度高,缺点:要求大片连续空间,改变容量不方便
每个结点除了存放数据元素外,还要存储指向下一个节点的指针,不要求大片连续空间,改变容量方便。
typedef 关键字 —— 数据类型重命名
typedef struct LNode
{
ElemType data;//数据域
struct LNode *next;//指针域 指向下一个结点
}LNode,*LinkList;
不带头结点初始化方法:
bool InitList(LinkList &L){
L=NULL; //防止脏数据
return true;
}
理解:对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑,对空表和非空表的处理需要用不同的代码逻辑
空表判断: L==NULL;
带头结点初始化:头结点不存数据,只是为了操作方便
bool InitList(LinkList &L){
L = (LinkList)malloc(sizeof(LNode)); //分配一个头结点
if(L==NULL){ //内存不足,失败判断,输出false
return false;
}
L->next=null;
return true;
}
空表判断: L->next==NULL;
头插法:实现较为简单,但是与输入顺序的数据不一致。
LinkList HeadCreatList(LinkList &L) //头插法建立链表
{
LNode *s;int x;
L = (LinkList)malloc(sizeof(LNode)); //初始化空表,申请一个头结点(创建头结点)
L->Next = NULL; //头指针为空
scanf("%d",&x); //输入值
for (int i = 0; i <99; i++)
{
s = (LNode*)malloc(sizeof(LNode)); //p指向新申请的结点
s->data=x;
s->next=L->next;
L->next=s; //新结点插入表中。
scanf("%d",&x); //输入值
}
return L;
}
时间复杂度: O(n)
LinkList List_TailInsert(LinkList &L){
int x; //设ElemType为整型
L=(LinkList)malloc(sizeof(LNode)); //建立头结点
LNode *s,*r=L; //r为表尾指针
scanf("%d",&x); //输入结点的值
while(x!=99){ //输入99表示结束
s =(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s; //r指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL; //尾结点指针置空
return L;
}
时间复杂度:O(n)
按位序插入(带头结点)
bool ListInsert(LinkList &L , int index , ElemType e){ // 插入
if(index < 1){
return false;
}
LNode *p = L; // 指向头节点
int j = 0 ;
while(p != NULL && j < index-1){ // 插入位序的前一个
p = p->next;
j++;
}
if(p == NULL){ //i值不合法
return false;
}
LNode *s = (LNode*)malloc (sizeof(LNode)); // 为插入的数据申请一个内存地址
s->data = e ;
s->next = p->next;
p->next = s;
return true;
}
按位序插入(不带头结点)
bool ListInsert(LinkList &L , int index , ElemType e){/// 不带头节点的
if(index < 1 )
return false;
if(index == 1){ // 不带头节点的要特别判段1这个位置,因为这是链表是空的 ,不能进行插入操作
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e ;
s->next = NULL;
L = s ; // 把头节点带回去
return true;
}
LNode *p = L;
int j = 1 ;
while(p!=NULL && j < index-1){
p = p->next ;
j++;
}
// 为插入的数据申请内存
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next ;
p->next = s;
return s; 插入成功
}
LNode * GetElem(LinkList L , int index){
if(index < 1)
return NULL;
LNode *p = L;
int j = 0 ;
while(p != NULL && j < index){
p = p->next;
j++;
}
if(p == NULL)
return NULL; // 没找到
return p;
}
LNode * LocateElem(LinkList L , ElemType e){ // 按值查找
LNode *p = L->next;
while(p != NULL && p->data != e){
p = p->next;
}
if(p == NULL)
return NULL;
return p;
}
先调用GetElem(L,i-1)查找第i-1个结点;
p=GetElem(L,i-1);
s->next=p->next;
p->next=s;
算法开销主要在查找上,如果在给定的结点后插入,则时间复杂度为O(1)
p=GetElem(L,i-1);
q=p—>next;
p->next=q-next;
free(q); //释放空间
算法开销主要在查找上,如果在给定的结点后删除,则时间复杂度为O(1)
出现:单链表无法逆向检索。
typedef struct DNode
{
ElemType data;//数据域
struct DNode *prior, *next;//前驱,后继指针域
}DNode,*DLinkList;
在p指的结点后插入结点*s
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
删除p后继结点的后继结点q
p->next=q->next;
q->next->prior=p;
free(q); //释放空间
循环单链表:表尾结点的next指针指向头结点。 (算法与单链表几乎一致,不同为如果在表尾进行操作,则操作步骤不同。)
循环双链表:表头结点的 prior 指向表尾结点;表尾结点的 next 指向头结点。
循环双链表插入
p结点后插入s
bool InsertNextDNode(DNode *p DNode *s){
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
}
循环双链表删除
//删除p的后继结点q
p->next=q->next;
q->next->prior=p;
free(q);
借助数组来描述线性表的链式储存结构。
静态链表的插入删除与动态链表的相同,只需要修改指针,而不需要移动元素。
优点:增、删 操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变。