前言
本文是看 bilibili · 王道考研 · 数据结构 的视频课程时做的一些记录,如构成侵权,请联系我删除
另外,本文有部分内容是根据我的理解写的,可能有不明确的地方 awa
王道考研的视频地址我就不放了,大家请移步 bilibili 直接搜索 王道考研 就能搜索到
线性结构 - 一对一关系
除了第一个元素之外,每个元素都有唯一前驱;除最后一个元素外,每个元素都有唯一后继
线性表 - Linear List
定义
-
线性表中的数据元素的 数据类型 相同
-
有限序列 - 元素之间有次序 + 元素有限
-
若 n 为表长,
n=0
时表示线性表为空表 -
线性表的角标是从1开始的,这个角标表示其位置,叫作位序
a1 叫作表头元素,an 叫作表尾元素
线性表:L=(a1,a2,a3,······,ai,······,an)
程序中数组的下标从0开始
基本操作
- 初始化线性表 - InitList(&L)
- 销毁线性表 - DestoryList(&L)
- 插入元素 - ListInsert(&L,i,e)
- 删除元素 - ListDelete(&L,i,&e) - e返回删除的值
- 按值查找 - LocateElem(L,e)
- 按位查找 - GetElem(L,i)
- 其他常见函数
- 求表长 - Length(L)
- 输出线性表元素 - PrintList(L)
- 判空 - Empty(L)
为什么要做这个基本操作
- 方便队友调用
- 减少同一功能的重复编写,降低出错率
其实可以理解为线性表是元素之间的逻辑关系,表达元素之间的线性结构组成,线性表更加符合使用场景中的描述【位序从a1开始,到an结束】
后续出现的顺序表、链表、栈、队列等都可以看成特殊的线性表,但是顺序表等数据结构更加考虑到程序设计和实现的情况【数组位序从a0开始】
顺序表 - 用顺序存储的方式实现线性表
定义
-
通过数据元素的大小 + 线性表首元素的地址 == 定位顺序表的元素位置
数据元素的大小:
sizeof(Elem Type)
会返回一个 值=Elem 的大小 -
特点
随机访问:在
O(1)
时间找到指定位置的元素存储密度高:每个节点只存储数据元素,(不存在指针等占用空间)
拓展容量不方便
数据的增删操作不方便
静态分配 - 这个很简单的
这个静态分配更加类似于数组的结构
#include<stdio.h>
#define MaxSize 20 //宏定义最大长度
typedef int ElemType; //定义数据类型
typedef struct
{
//顺序表最大容量
//程序运行时,内存就会分配MaxSize*sizeof(Elem Type)的空间给程序
ElemType data[MaxSize];
int length; //顺序表长度(元素个数)
}SqList;
//初始化顺序表
void InitList(SqList &L)
{
for(int h=0;h<L.length;h++)
L.data[h]=0; //此步骤可以省略,详细解释如下
L.length=0;
}
初始化顺序表:可以省略,不过如果不进行初始化,数组可能获取到之前遗留的脏数据,导致没有进行赋值的数据元素呈现怪异值
脏数据:多数是"没有进行初始化+没有进行赋值"的数据,读取了内存中残留的数据
C语言为数组的初始值设为 0:这个其实和编译器有关,并不是一定的
动态分配 - 解决静态分配顺序表长度不可变的问题
这个动态分配就更加类似于 动态数组 - 用数组头结点指针表示数组
-
指针指向动态顺序表的第一个元素的地址
-
malloc函数、free函数 - (C++ 可以使用new函数、delete函数)
malloc函数、free函数包含在头文件
stdlib.h
中malooc每次都向内存申请一片新的连续的地址,返回一个无类型的、指向这片地址的首地址 的指针
无类型:所以使用malloc时,需要在最后强制转换返回值类型为我定义的数据元素类型的指针申请空间的大小由malloc的参数决定
//此处的InitSize为顺序表的初始长度,malloc也是首次申请内存 //L.data指向动态顺序表的第一个元素地址 L.data=(ElemType *)malloc(sizeof(ElemType)*InitSize);
//realloc 可以实现动态数组的扩容,不过此处不展示
#include<stdio.h>
#include<stdlib.h>
#define InitSize 10 // 最开始时顺序表容量
#define creaselen 10 // 每次扩容的长度
typedef int ElemType;
typedef struct
{
int * data; //顺序表首地址
int length; //顺序表元素个数
int MaxSize;//顺序表最大容量
}SeqList;
//初始化顺序表
void InitList(SeqList &L)
{
// 这边注意一个地方:sizeof(ElemType),而不是SeqList
L.data=(ElemType *)malloc(InitSize*(sizeof(ElemType)));
L.length=0;
L.MaxSize=InitSize;
}
//给顺序表扩容
//基本思路:先用p保存数据,然后给L一个新&&大的空间,然后将p的数据复制到L
void IncreaseSize(SeqList &L)
{
ElemType * p; // p为ElemType的指针类型变量
p=L.data;
L.data=(ElemType *)malloc((L.MaxSize+creaselen)*sizeof(ElemType));
for(int h=0;h<L.length;h++)
// 用法有点像数组,可以去看看动态数组-指针数组
L.data[h]=p[h];
L.MaxSize=L.MaxSize+creaselen;
free(p);
}
基本操作及代码 + 时间复杂度分析
//假设i位置在1~L.MaxSize之间
//在线性表中的第i位置插入数据e
void ListInsert(SeqList &L,int i,ElemType e)
{
i--; //位序和数组序转换一下【位序 == 数组下标 + 1】
if(L.length+1>L.MaxSize)
IncreaseSize(L);
for(int h=L.length;h>=i;h--)
L.data[h]=L.data[h-1];
L.data[i]=e;
L.length++;
}
/*时间复杂度分析
1. 最好情况:插入到表尾,T(n)=O(1)
2. 最坏情况:插入到表头,T(n)=O(n)
3. 平均情况:假设新元素插入到任何位置的概率(p)相同
T(n)=np+(n-1)p+(n-2)p+···+2p+p,取极限为n/2,T(n)=O(n)
*/
//----------------------------------------------------------
//删除表中第i位置的元素,并用e返回删除值
void ListDelete(SeqList &L,int i,ElemType &e)
{
//位序和数组序转换
i--;
e=L.data[i];
for(int h=i;h<L.length-1;h++)
L.data[h]=L.data[h+1];
L.data[L.length-1]=0; //初始化
L.length--;
}
/*时间复杂度分析
同'在线性表中的第i位置插入数据e',不同点在于删除操作在局部上看,需要操作的次数比插入操作少一次,但是在讨论时间复杂度的整体来看,差别不大
1. 最好情况:T(n)=O(1)
2. 最坏情况:T(n)=n-1=O(n)
3. 平均情况:T(n)=(n-1)/2=O(n)
*/
//----------------------------------------------------------
//按值查找 == 返回值所在的第一个位置 ,如果没有,返回0
int LocateElem(SeqList L,ElemType e)
{
for(int h=0;h<L.length;h++)
if(L.data[h]==e)
return (h+1);
return -1;
}
/*时间复杂度分析
同'在线性表中的第i位置插入数据e'的时间复杂度
*/
//----------------------------------------------------------
//按位查找 ,i表示第i位置
ElemType GetElem(SeqList L,int i)
{
i--;
return L.data[i];
}
/*时间复杂度分析
T(n)=1
*/
//----------------------------------------------------------
//输出顺序表元素
void PrintList(SeqList L)
{
for(int h=0;h<L.length;h++)
printf("%d ",L.data[h]);
printf("\n");
}
链表 - 链式结构实现线性表
单链表 - 每个节点需要存储下一节点的地址
不要求大片连续的空间,改变容量方便 + 但是不能随机存储,要耗费一定空间存放指针 我觉得应该还有增删操作不便【但是比起顺序表来说那还是方便多了】
- 不带头结点
- 带头结点 - 更方便 —— 所以一般采用带头结点的方式
单链表的定义
LNode * 和 LinkList【图片来自王道考研视频】
// 不带头结点
#define ElemType int // 其实这个预定义的 ElemType 也可以用 typedef 实现
typedef struct LNode //这个LNode是为了在下面定义指针时使用 即 LNode* next
{
ElemType data;
LNode * next; // 这边可以不加 struct是因为我用的 C++
}LNode,* LinkList;
/*
// 以上代码等价于
struct LNode
{
ElemType data;
LNode * next; // 这边可以不加 struct是因为我用的 C++
};
typedef LNode LNode;
typedef LNode * LinkList;
*/
// 初始化
bool InitList(LinkList &L)
{
L=NULL; // 防止脏数据
return true;
}
// 带头结点
#define ElemType int
typedef struct LNode
{
ElemType data;
LNode * next; // 这边可以不加 struct是因为我用的 C++
}LNode,* LinkList;
// 初始化
bool InitList(LinkList &L)
{
L=(LNode *)malloc(sizeof(LNode)); // 分配一个头结点
if(L==NULL) // 没分配到空间
return false;
else
{
L->next=NULL; // L为指针用 ->
return true;
}
}
单链表的基本操作
带头结点
// 按位序插入 - 在 i 号位置上插入元素 e
bool ListInsert(LinkList &L,int i,ElemType e)
{
// 判断 i 合法性 ···
if(i<1) return false; // 默认头节点为 0号节点
LNode* q=L; // 遍历链表
while(q!=NULL&&i>1) // 目的就是找到第 i-1 的结点【位序】
{
i--;
q=q->next;
}
if(q==NULL) return false;
// i 值不合法 - i所在位置没有前驱节点
// 如 往链表[1,2]中插入6号位置节点
// 使用上述判断能够成功的原因是,我们需要前驱节点做链接,遍历到插入位置的前一个位置就停止
LNode* p=(LNode*)malloc(sizeof(LNode));
if(p==NULL) return false; // 没有申请到空间
LNode* temp=q->next;
q->next=p;
p->next=temp;
p->data=e;
return true;
}
//上述代码的插入方式平均时间复杂度为O(n) -- 很好理解的吧
// 输出函数
void ListOut(LinkList L)
{
printf("\n链表输出:\n");
LNode* q=L;
q=q->next; //隐藏头节点
while(q!=NULL)
{
printf("%d ",q->data);
q=q->next;
}
}
// 向指针形式的结点 p 之后插入元素 e ===== 时间复杂度O(1)
bool InsertNextNode(LNode* p,ElemType e)
{
//【需要判断 p 的存在噢】
if(p==NULL) return false;
LNode* q=(LNode*)malloc(sizeof(LNode));
if(q==NULL) return false;
q->data=e;
LNode* temp;
temp=p->next;
p->next=q;
q->next=temp;
return true;
}
// 向指针形式的结点 p 之前插入元素e ===== 时间复杂度O(n)
bool InsertPriorNode(LNode* p,ElemType e)
{
if(p==NULL) return false;
LNode* q=(LNode*)malloc(sizeof(LNode));
if(q==NULL) return false;
q->data=e;
LNode* prior=L; // 这里的 L 不声明是因为我在最前面声明了全局变量【大聪明】
while(prior->next!=p) prior=prior->next;
prior->next=q;
q->next=p;
return true;
}
// 向指针形式的结点 p 之前插入元素e-交换数据法 【这可真是个小机灵鬼啊】 ===== 时间复杂度O(1)
bool InsertPriorNodeSwap(LNode* p,ElemType e)
{
if(p==NULL) return false;
LNode* q=(LNode*)malloc(sizeof(LNode));
if(q==NULL) return false;
// 先使用后插法,将新结点插到 p 后面,然后,交换 p 和新结点的数据
LNode* temp=p->next;
p->next=q;
q->next=temp;
q->data=p->data;
p->data=e;
return true;
}
当链表 L 作为全局变量在程序开头声明时的情况,L 本身参与各个函数运算,因此,函数会对 L 本身做出改变的时候,可以不用 &
向指针形式的结点 p 之前插入元素e-交换数据法
这个方法有个很明显的 BUG:无法处理【向头结点之前插入元素】的操作
// 删除位序 i 的结点,并返回结点 i 的值
bool ListDelete(int i,ElemType &e)
{
if(i<1) return false;
LNode* p=L;
while(p!=NULL&&i>1) // 定位到 i-1 结点,记为 p
{
i--;
p=p->next;
}
if(p==NULL) return false; // i-1 超出范围
LNode* Nodei=p->next; // i 结点
e=Nodei->data;
p->next=Nodei->next;
free(Nodei);
return true;
}
// 删除指针形式的结点 p,并返回 p 结点的值
bool DeleteNode(LinkList &L,LNode* p,ElemType &e)
{
if(p==NULL) return false; // p 指针判空
LNode* ReadNode=L; // ReadNode 为 p 结点的前一个结点
while(ReadNode->next!=p) ReadNode=ReadNode->next;
ReadNode->next=p->next;
e=p->data;
free(p);
return true;
}
本篇涉及【参数为指针形式的结点】时,不考虑结点不在单链表之内的情况
删除指针形式的结点 p,并返回 p 结点的值
这个也有交换【结点 p | 结点 p 之后的结点】的值的做法,但是,明显,有无法完成删除尾节点操作的BUG
// 查找 - 按位查找 查找位序为 i 的结点并返回结点指针
// 这个返回 NULL 是我自己发挥的,在主程序运行会出问题的
LNode* GetElem(LinkList L,int i)
{
// 考虑头结点作为 0 位序结点,不能被查找
if(i<1) return NULL;
LNode* ansnode=L;
while(ansnode!=NULL&&i>0)
{
ansnode=ansnode->next;
i--;
}
if(ansnode==NULL) return NULL;
return ansnode;
}
int elem_pos; // 服务于主函数的输出,作为查找函数并不需要
// 查找 - 按值查找 查找到值为 e 的结点,并返回该结点指针
LNode* LocateElem(LinkList L,ElemType e)
{
elem_pos=1; // 服务于主函数的输出,作为查找函数并不需要
LNode* p=L->next; // 不从L开始是因为本篇的头结点在逻辑上是没有data的
while(p!=NULL&&p->data!=e)
{
p=p->next;
elem_pos++; // 服务于主函数的输出,作为查找函数并不需要
}
if(p==NULL)
{
elem_pos=-1; // 服务于主函数的输出,作为查找函数并不需要
return NULL;
}
return p;
}
// 求表长
int Length(LinkList L)
{
int len=-1;
LNode* p=L;
while(p!=NULL)
{
p=p->next;
len++;
}
return len;
}
// 尾插法构建单链表
LinkList List_TailInsert(LinkList &L)
{
int n; // 每次向单链表中插入的元素值
L= (LinkList)malloc(sizeof(LNode)); // 声明头结点,使用 LNode* 作为 malloc 返回类型也行
L->data=0; // 我自己需要的补充赋值
LNode* tail=L; // 用作标记尾节点
LNode* temp; // 用作声明每次新增的结点
while(scanf("%d",&n)&&n!=-1) // 当输入的值不为 -1 时,将该值插入链表
{
temp=(LNode*)malloc(sizeof(LNode));
temp->data=n;
tail->next=temp;
tail=temp;
}
tail->next=NULL; // 尾节点的 next 赋值为 NULL
return L;
}
// 头插法构建单链表
LinkList List_HeadInsert(LinkList &L)
{
int n;
L=(LinkList)malloc(sizeof(LNode));
L->data=0; // 补充赋值
L->next=NULL; // 赋值为 NULL
// 1. 之后 temp->next=L->next; 操作需要 next | 2. 防止默认的 next 指针指向内存的奇怪位置
LNode* temp; // 新加入的结点
while(scanf("%d",&n)&&n!=-1)
{
temp=(LNode*)malloc(sizeof(LNode));
temp->data=n;
temp->next=L->next;
L->next=temp;
}
return L;
}
头插法的重要使用场景:链表的逆置
不带头节点
// 按位序插入 - 在位置 i 的地方插入 e
bool ListInsert(LinkList &L,int i,ElemType e)
{
if(i<=0) return false; // 判断 i 合法性
LNode* p=(LNode*)malloc(sizeof(LNode)); // 申请一个结点空间放要插入的东西
if(p==NULL) return false; // 没有申请到新结点的空间
p->data=e;
p->next=NULL;
if(i==1)
{
// LNode* temp=L;
// L=p;
// L->next=temp; 下面这种写法很明显优于我的awa
p->next=L;
L=p;
return true;
}
LNode* q=L; // 记录 i-1 的结点
/*
注意:为啥这里是 i>2 ,对比"带头结点的",目的都是找到 i-1 位序的元素指针
1. 对于带头结点的: i-1 位序 ==> 头结点0 结点1 ··· 结点i-1 结点i
2. 对于不带头结点的:i-1 位序 ==> 头结点1 结点2 ··· 结点i-1 结点i
因此,不带头结点的单链表,遍历到 i-1 需要比带头结点的单链表少移动一次
则【如上:1.到结点 1 移动 1 次,2.到结点 1 移动 0 次】
*/
while(q!=NULL&&i>2)
{
i--;
q=q->next;
}
LNode* temp=q->next;
q->next=p;
p->next=temp;
return true;
}
// 输出函数
void ListOut(LinkList L)
{
LNode* q=L;
printf("单链表为:\n");
while(q!=NULL)
{
printf("%d ",q->data);
q=q->next;
}
printf("\n");
}
// 求表长
int Length(LinkList L)
{
int len=0;
LNode* p=L;
while(p!=NULL)
{
p=p->next;
len++;
}
return len;
}
// 尾插法构建单链表
LinkList List_TailInsert(LinkList &L)
{
int n;
L=(LinkList)malloc(sizeof(LNode));
L->data=NULL; // 提前赋值,防止出现直接输入 -1【即建立空链表】时出现 data 为存在的任意值
// 因为尾插法的核心思想是:在一个记录尾结点的 tailnode 后不断插入新结点
// 当为不带头结点的链表时,第一个尾结点为逻辑上不存在的链表头结点
LNode* tailnode=L; // 尾结点
LNode* tempnode; // 新结点
bool flag=true; // 计数,记录当前被插入的结点是否应该是头结点
// 不能直接使用 else 中的语句,要考虑到第一个结点一定要变成头结点 L 的问题
while(scanf("%d",&n)&&n!=-1)
{
if(flag)
{
tailnode->data=n;
flag=false;
}
else
{
tempnode=(LNode*)malloc(sizeof(LNode));
tempnode->data=n;
tailnode->next=tempnode;
tailnode=tempnode;
}
}
tailnode->next=NULL; // 尾结点 next 赋空值
return L;
}
// 头插法构建单链表
LinkList List_HeadInsert(LinkList &L)
{
int n;
L=(LinkList)malloc(sizeof(LNode));
// 初始化 L,防止建立空链表时出错
L->data=NULL;
L->next=NULL;
LNode* temp;
bool flag=true;
while(scanf("%d",&n)&&n!=-1)
{
if(flag)
{
L->data=n;
flag=false;
}
else
{
temp=(LNode*)malloc(sizeof(LNode));
temp->data=n;
temp->next=L;
L=temp;
}
}
return L;
}
头插法的思想是:
新结点的 next 是原链表的头结点,然后原链表头结点换成现在的新结点
而不是 新结点成为头结点,然后继承原来头结点的后继
双链表
基础理解:双链表就是设计一个数据结构 DNode ,这个结构中含有数据项
数据
前驱节点指针
后继节点指针双链表是一个多了前驱结点指针的单链表,指针双向,因此称为双链表
双链表的定义
typedef struct DNode
{
ElemType data;
DNode* prior; // 前驱结点
DNode* next; // 后继结点
} * DLinkList, DNode;
双链表的基本操作【伪代码】
// 初始化
// 注意, 带头结点的双链表 L->next=NULL; 注意区分双链表和循环链表
// 双链表和单链表原则上是一致的,只是比单链表多加一个指向前驱结点的指针
bool InitDLinkList(DLinkList &L)
{
L=(DNode*)malloc(sizeof(DNode));
if(L==NULL) return false;
L->next=NULL;
L->prior=NULL; // 此处的头结点的前驱结点其实确定 == NULL
return true;
}
// 判空
bool isEmpty(DLinkList L)
{
/*
if(L->next==NULL) ==> isEmpty
*/
}
// 在 p 结点之后插入一个结点 s
// 由于,s 结点是会修改其前驱后继结点的,因此需要取地址符
// 需要着重注意的是:p 结点有可能是尾结点
bool InsertNextDNode(DLinkList &L,DNode* p,DNode* s)
{
/*
if(p->next==NULL)
p->next=s;
s->prior=p;
s->next=NULL;
else
DNode* oldnext=p->next;
oldnext->prior=s;
s->next=oldnext;
p->next=s;
s->prior=p;
// =========================================== //
DNode* oldnext=p->next;
p->next=s;
s->next=oldnext;
s->prior=p;
if(oldnext!=NULL)
oldnext->prior=s; // 王道考研的视频写为 p->next->prior=s; 但是我觉得我写得好些
*/
}
// 删除 结点 p 的后继结点
// 注意:p 结点 的后继结点是尾结点的情况
bool DeleteNextDNode(DLinkList &L)
{
/*
DNode* nextNode=p->next;
if(nextNode==NULL) return false; // 排除 p 没有后继结点的情况
p->next=nextNode->next;
if(nextNode->next!=NULL)
nextNode->next->prior=p; // 依然不建议使用这种方式
free(nextNode);
return true;
*/
}
// 删除一个双链表
bool DestoryList(DLinkList &L)
{
/*
//遍历链表,删除所有结点,最后 L 指向 NULL
DNode* readNode=L;
while(readNode!=NULL)
DNode* temp=readNode;
readNode=readNode->next;
free(temp);
L=NULL;
// =============================================================== //
// 王道考研的思路是:删除头结点后的每个结点,调用 DeleteNextNode 函数 ,最后释放 L
*/
}
// 双链表的前向遍历 | 双链表的后向遍历 ==> p 结点向前或向后遍历
// 注意前向遍历到头结点的时候的处理
void ReadForeward(DLinkList L,DNode* p)
{
// 前向遍历的循环结束条件是 readNode->prior==NULL
}
void ReadBackward(DLinkList L,DNode* p)
{
}
// 按位查找 | 按值查找
/*
其实按位查找和按值查找的操作和单链表几乎一样,甚至可以一样,时间复杂度 O(n)
*/
DNode FindbyPos(int i)
{
}
DNode FindbyValue(ElemType e)
{
}
循环链表
循环链表是一个首位相连的单链表或者双链表
虽然循环链表与普通的单链表、双链表没太大的差距,但是循环链表有其特殊的应用场景:
对于需要频繁操作表头、表尾的链表,可以使用循环链表|| 利用
尾结点 -> next == 头结点
的特性,可以将头尾操作控制在时间复杂度 O(1) ,即每次操作都针对尾结点
|| 需要注意 == 链表指针指向尾结点,这样才能每次直接通过链表指针找到尾结点
|| 需要注意 == 当链表尾结点发生变化时,也要同步链表指针的变化
对于循环链表的定义与操作和单链表、双链表相似,只是需要特殊注意头结点和尾结点的处理
循环单链表
首尾相接的单链表:尾结点 -> next = 头结点
链表初始状态为
L -> next = L;
循环双链表
首尾相接的双链表:尾结点 ->next = 头结点 && 头结点 -> prior = 尾结点
链表初始状态为
L -> next = L;
此时也有L -> prior = L;
但是 判断循环双链表是否为空 可以只需要一个条件L -> next = L;
静态链表
- 由代码可知:这个静态链表物理结构上其实是一个数组 StsticNode array[]
因此,这个链表在内存中分配到的是一整片位置
同时具有数组的性质:只要知道数组的0下标指针,那么其余下标的元素的地址可计算,数组的元素平均分配一整片空间,每个元素分配到的大小为sizeof(StaticNode)
- 代码的逻辑关系是:带头结点的单链表。
数据项 data 对应结点的值,next 对应本结点的后继结点的数组下标 - 实现逻辑比较灵活,如:
StaticLinkList[0]
为头结点,
StaticLinkList[0].next=2;
表示头结点的下一个结点(后继结点)在数组下标为2的地方,
即StaticLinkList[2]
,这个结点的下一个结点可能是数组任意一个位置
尾结点的next标记为-1 - 空链表可以表示为
StaticLinkList[0].next=-1;
- 在定义静态链表的时候,规定
next = -2;
为了之后取用数据结点时,判断结点是否还没被使用
#include<stdio.h>
#include<stdlib.h>
#define Maxsize 100
#define ElemType int
// 以上 define 语句可替换成 typedef int ElemType; [注意需要 ; ]
typedef struct
{
ElemType data;
int next=-2;
} StaticLinkList[Maxsize];
/*
上面这种写法确实很少见,毕竟一般我们很少这样定义,我们的一般写法是
struct StaticNode
{
ElemType data;
int next=-2;
};
typedef StaticNode StaticLinkList[Maxsize];
其中需要注意:
1. struct本身的使用法就带有分号 struct Name{ };
2. typedef本身的使用法也带有分号 typedef struct newname;
3. typedef StaticNode StaticLinkList[Maxsize];
代码特殊说明:按照 C 语言的使用方式,StaticNode 前面需要加上 struct
声明 StaticLinkList 为大小为 Maxsize 的 StaticNode 类型的数组 [注意声明数组的写法]
*/
// 由以上代码可知:StaticLinkList == StaticNode array[Maxsize];
// 以上写法是为了代码的可读性,StaticLinkList声明的数据结构为一个静态链表,array声明的是数组
总的来说,静态链表是一个不怎么好用同时又很灵活的单链表。
普通的单链表的结点根据系统分配储存在内存的未知空间,静态链表的结点位置由结构代码决定
顺序表 | 链表 【比较】
复习一下之前的顺序表,顺序表类似于数组
静态实现方式:是标记了元素个数的数组
动态实现方式:是标记了大小、元素个数的动态数组(指针数组)对于动态数组,理解是:
ElemType* array; array=(ElemType*)malloc(Maxsize*sizeof(ElemType)); // 以上代码的意思:声明一个指针 array 作为动态数组的头结点 // 以头结点为基础,申请一片连续空间。空间中包含 Maxsize 个 ElemType 元素的大小
顺序表和链表在逻辑关系上:线性表
顺序表和链表在物理关系上:略
顺序表的销毁:需要修改数据项
length=0;
对于顺序表的静态实现方式,设置 length = 0 之后,系统会自动回收空间
对于顺序表的动态实现方式,设置 length = 0 之后,还需要free(L.data);
回收数据项 data 所代表的动态数组
需要额外注意,从代码逻辑层面分析,顺序表和链表 增、删 操作的时间复杂度为 O(n)
但是
考虑到实际物理分析,顺序表对应的增删操作还要附带其余结点的移动时间,因此,对于数据量大或者数据量不确定的应用场景,选择链表更加适用 == 链表应对增删操作用的实际时间更短
栈【后进先出LIFO】
栈:是只能对一端进行增删操作的线性表。
n个互异的元素出栈的顺序情况数量【图片来自王道考研视频】
如:对 a b c d e 的合法出栈的情况:b c e d a【a in | b in | b out | c in | c out | d in | e in | e out | d out | a out】
顺序栈
理解上来说:顺序栈就是使用数组实现的栈,栈的大小不能变化
实现思想:
数据项ElemType data[Maxsize]
储存栈中的数据
数据项top
标记栈顶所对应的游标
顺序栈的定义
struct SqStack
{
ElemType data[Maxsize];
int top=-1; // 空栈,如果不提前设置为 -1,则需要 InitSqStack 函数修改 top 进行初始化
}; // 当然这种定义结构体的方式是我喜欢用的,数据结构更喜欢连带 typedef 使用
当然,标记栈顶的 top 可以是别的位置
如,top = 0 表示空栈的设计,即 top 标记栈顶的下一个游标(数组下标)
此时,需要注意操作写法:满栈对应 top == Maxsize,Push、Pop操作注意操作顺序
顺序栈对应的基本操作
// 判空 [空栈返回 true]
bool isEmpty(SqStack S)
{
if(S.top==-1) return true;
return false;
}
// 进栈 Push
bool Push(SqStack &S,ElemType e)
{
if(S.top+1==Maxsize) return false; // 满栈
S.top++;
S.data[S.top]=e; // 核心代码 S.data[++S.top]=e;
return true;
}
// 出栈 Pop [删除栈顶 + 栈顶元素由 e 接收]
bool Pop(SqStack &S,ElemType &e)
{
if(S.top == -1) return false;
e=S.data[S.top];
top--; // 核心代码 e=S.data[S.top--];
return true;
}
// 读栈顶
bool GetTop(SqStack S,ElemType &e)
{
if(S.top == -1) return false;
e=S.data[S.top];
return true;
}
共享栈
两个栈共用同一片空间,两个栈分别从空间的两端向中间进栈,其中 top0 是栈0的栈顶标记,top1是栈1的栈顶标记
top0 = -1;
top1 = Maxsize;
为共享栈的初始状态
当 top0 + 1 == top1;
表示满栈
链栈
使用链表实现的栈
typedef struct StackNode { ElemType data; StackNode* next; } * LStack;
说明:
链栈的实现思路是:头结点 | 头结点的下一结点 作为栈顶,对 头结点 | 头结点的下一结点 进行操作
带头结点
// 初始化 | 空栈
L->next == NULL;
// 入栈 Push
StackNode* temp = (StackNode*)malloc(sizeof(StackNode));
temp.data = e;
temp.next = L->next;
L->next = temp;
// 出栈 Pop
StackNode* temp = L->next;
e = temp.data; // 返回栈顶元素
L->next = temp->next;
//free(temp);
注意!!!
出栈和获取栈顶元素需要判定是否栈空
不带头结点
// 初始化 | 空栈
L == NULL;
// 入栈 Push
StackNode* temp = (StackNode*)malloc(sizeof(StackNode));
temp.data = e;
temp->next = L;
L = temp;
// 出栈 Pop
StackNode* temp = L;
e = temp.data;
L = temp->next;
//free(temp);
队列【先进先出FIFO】
顺序队列
普通顺序队列
这就不用多说了 数组 + 下标表示队首队尾
- 队尾标记为最后插入元素的位置
- 队尾标记为最后插入元素的下一个位置
循环队列
在普通队列的基础上,进行以下改进【为了利用空间awa】
- 将队列看成环状,队首之前的数组空间重复利用
- 每次入队时进行操作
(rear + 1)%Maxsize == front;
满队
如果队列不满rear++; rear = rear%Maxsize
- 每次出队时进行操作
判空
front = (front+1)%Maxsize;
根据队尾下标的位置,可以有两种初始化选择
front = 0; rear = 0;
front = 0; rea = Maxsize - 1;
特殊设计队列
以上循环队列还是需要一个空数组元素,在判断为满队列时 需要用到一个空数组位置来区分。
因此
- 当 rear 指向队尾的下一个元素,
rear + 1 == front;
为队满,此时,rear所在位置不能进行入队,如果入队,rear将和front指向同一位置,与空队判定产生冲突 - 当 rear 指向队尾元素,rear在初始化时,
rear = Maxsize - 1;
,因此array[Maxsize-1]
不能是满队列的队尾
解决办法
- 设置 size 变量,记录队列元素个数
- 设置 tag 变量,记录上一次操作类型。
产生插入操作 ==> tag = 1 ==> 只可能导致满队列
产生删除操作 ==> tag = 0 ==> 只可能导致空队列
链队列
typedef struct LNode
{
ElemType data;
LNode* next;
} LNode;
typedef struct Queue
{
LNode* front;
LNode* rear;
}Queue;
带头结点
这个比较简单
但是注意 :出队之后为空队列的情况【需要改变 front rear】
不带头结点
注意:入队第一个元素有略微的不同,但是也很简单
但是注意 :出队之后为空队列的情况【需要改变 front rear】
双端队列
这是一个可容许队列从两端进行操作的队列
队列是双端队列的一种
栈是双端队列的一种
输入受限的双端队列【一端输入,两端输出】
输出受限的双端队列【一端输出,两端输入】
一般考察:以下哪些次序不能由双端队列的出队操作得到 等
栈 | 队列 - 具体使用场景
括号匹配
// 编程要求:输入一段括号字符串,判断字符串是否为合格括号对
#include<stdio.h>
#define Maxsize 100
#define ElemType char
struct SqStack
{
ElemType data[Maxsize];
int top=0;
};
// 压栈
void Push(SqStack &S,ElemType temp)
{
if(S.top == Maxsize)
{
printf("栈满\n");
return ;
}
S.data[S.top] = temp;
S.top++;
}
// 判空
bool isEmpty(SqStack S)
{
if(S.top == 0) return true;
else return false;
}
// 判定括号是否匹配
bool isMarch(SqStack S,ElemType temp)
{
// 如果是空栈,返回不匹配
if(isEmpty(S)) return false;
// 获取栈顶元素
char topc = S.data[S.top-1];
if(temp == ')' && topc == '(') return true;
else if(temp == ']' && topc == '[') return true;
else if(temp == '}' && topc == '{') return true;
else return false;
}
// 弹栈
void Pop(SqStack &S)
{
// 如果空栈,则形成错误提示
// 但其实按照代码逻辑,这一步发生在 isMarch 函数之后,在判断匹配时,就已经进行判空,这一步应该可以省略
if(isEmpty(S))
{
printf("空栈\n");
return ;
}
S.top--;
}
int main()
{
SqStack S;
char tempc;
while(scanf("%c",&tempc))
{
if(tempc == '\n') break;
if(tempc == '(' || tempc == '[' || tempc == '{')
Push(S,tempc);
if(tempc == ')' || tempc == ']' || tempc == '}')
{
if(isMarch(S,tempc)) Pop(S);
else
{
printf("括号-括号不匹配\n");
break;
}
}
}
if(isEmpty(S)) printf("括号匹配\n");
else printf("非空栈-括号不匹配\n");
return 0;
}