数据结构——线性结构
数据结构——线性结构篇
1、线性表
基本介绍
描述
线性表是指由同一类型的数据元素构成的有序序列的线性结构
概念
顺序存储:
顺序存储指在内存中开辟连续的存储空间来存放数据,比较有代表性的就是C语言中的数组。由于是使用下标直接进行各个元素的访问,于是查询、遍历效率高,但是当增删数据的时候,大概率是要对大部分元素的位置进行调整的,所以增删数据效率低
链式存储:
链式存储指通过指针来指向下一个元素或者上一个元素的地址来保证元素之间可以相互访问到,链式存储开辟的空间不一定是连续的,每一个结点所占的内存空间由数据域与指针域(也叫做地址域,单向链表只有尾指针,双向链表有头指针和尾指针)两部分构成。由于使用的是指针将各个元素串联起来,进行增删数据的时候,直接修改结点的指针即可,于是增删数据效率高,但是在进行元素查找的时候,由于不能直接访问到每一个元素,需要游标指针从链表的一端,不断移动,到达目标结点才能进行数据提取,于是查询、遍历效率低
前驱:
一个元素的前驱指该元素(非首元素)的前一元素
后继:
一个元素的后继指该元素(非尾元素)的后一元素
下标:
从0开始计算的与各个元素一一对应的数字
位序:
从1开始计算的与各个元素一一对应的数字(在下面的代码实现中,可能会发现位序的值小于1了,那仅仅是为了方便大家理解代码,真正的位序是大于等于1的)
在位序为i的地方插入元素表示:在线性表的第i个元素(位序为i的元素)的前面(左边为前面,右边为后面)插入元素。简单来说,就是插入操作完成以后,新元素的位序为i
位序为i的元素的下标为i-1
最后一个元素的位序即为整个线性表的长度
链表的头节点:
头节点是我们为了链表代码实现的方便,而创造出来的。具有了头节点的链表,进行结点的插入和删除的代码实现要简洁许多,因为不必把在链表真正的第一个结点之前插入新结点的情况使用单独的代码进行处理。注意,头节点为一个和后续普通节点具有相同数据项的数据元素,即头节点对应的结构体类型和后续普通节点的对应的结构体类型必须要是相同的,否则此为假的头节点,可能不会对代码编写起到便利作用(比如数据结构——图篇的邻接表表头就是一个假的头节点)
小贴士
为什么链表没有判满操作?
因为链表中的结点所占空间都是动态开辟的,根据动态开辟内存空间的特点,除非内存空间无法开辟(但是在代码正确的情况下基本是不会出现的),即内存专门用于动态开辟的空间已满,否则皆可进行链表的插入操作;而顺序存储下,由于存储数据的空间大小在数据结构创建以后是固定的,后续基本不会进行空间的扩充,于是有数据已满的情况(但是在后面的代码中我也提供了一种扩充顺序存储空间的方法,这个并不属于基本操作,一般不会使用,即固定的空间已满,一般不会进行扩容)
为什么在链表实现过程中,插入和删除结点的那几行代码不能调换顺序?
因为在链表实现过程中,不能在操作过程中把后续的部分链表丢失了,即要确保每一次操作,链表中所有的节点都可以直接或者间接被访问到,否则会造成内层泄漏。插入和删除结点的那几行代码如果调换顺序,如果读者画出内存布局图,就会很容易发现后续的部分链表彻底丢失了,无法找回
为什么循环队列的顺序实现中初始化的时候,两个指针皆赋值为0而不是-1?
因为如果使用的是-1,如果Front始终不移动(即Front始终等于-1),按照循环队列的循环的数学原理(即对指针先进行+1,再取余),明显一个数取余不会永远等于-1,这就可能会导致判满出现问题(具体是在"使用少用一个元素空间的方法"和"使用记录当前最后一次操作为入队还是出队的参数flag"情况下的判满操作)
在链表中实现数据的删除,是否必须要得到指向这个数据所在结点的前驱结点的指针才能进行?
不一定,在特殊情况下是不需要的
条件:如果这个结点不是链表的最后一个结点,而且现在有一个指向该结点的指针
操作:可以将下一个结点的数据赋值给本结点,然后把下一个结点作为替罪羊删除掉。这个思想在数据结构——二叉树篇的"二叉搜索树删除儿女双全的结点"的代码实现中也会运用到,即不是真的去删除目标数据所在结点,而是对要删除的数据直接进行覆盖,转化删除目标,去删除掉替罪羊结点即可
为什么无头节点的链式存储中插入和删除的操作需要返回的是指针类型的变量,而不是仅仅返回一个自定义bool类型的变量来仅仅判断是否操作成功?
C语言中,函数的形参是实参的一份临时拷贝,如果改变形参值,实参是不会发生相应变化的,不管是否二者的变量名相同,都是如此
如果返回的是bool类型,即仅仅只把操作是否成功的标识返回,那确实,对于这次操作是没有任何问题的,但是对于接下来的操作可能就会出现一些错乱,因为在这次操作以后,实参指针并没有发生改变,还是保存着操作之前的地址
当然,对于无头节点的链式存储中插入和删除的操作的函数设计,也可以不返回指针类型,读者可以选择传入参数的时候,把所要用到指针变量的地址传入,使用该指针变量的时候进行*解引用就可以了,这样每一次修改就会真真正正地改变那个指针变量存储的地址
代码实现
顺序存储
顺序存储
结构定义
typedef struct LNode
{
int* data; //data指向的就是真正用来存储数据的线性区域
int last; //last变量保存着存储的最后一个元素的【下标】
int maxsize; //maxsize变量保存着数据的最大容量
}*List;
(1)初始化
List Init_List(int max)
{
List L = (List)malloc(sizeof(struct LNode)); //动态分配内存空间
L->data = (int*)malloc(max * sizeof(int)); //为数据区域动态分配空间
L->last = -1; //由于初始没有任何元素,于是last初始化为-1
L->maxsize = max;
return L;
}
(2)判空
bool IsEmpty(List L)
{
return L->last == -1;
}
(3)判满
bool IsFull(List L)
{
return L->last + 1 == L->maxsize;
}
(4)求表长
bool Len(List L)
{
return L->last + 1;
}
(5)根据指定元素查找下标
int Find(List L, int val)
{
int i = 0;
while (i <= L->last && L->data[i] != val) //只有两种情况才会跳出循环:1.i>L->last 2.L->data[i] == val
i++;
if (i > L->last)
{
printf("元素%d不存在",val);
return ERROR;//根据实际情况#define 一下ERROR
}
else //L->data[i] == val的情况
return i;
}
(6)根据指定位序查找数据
int Find(List L, int index)
{
if (index<1 || index>L->last + 1)
return ERROR; //根据实际情况#define 一下 ERROR
else
return L->data[index - 1];
}
(7)根据指定位序插入
bool Insert(List L, int index, int val)
{
if (IsFull(L)) return false;
if (index<1 || index>L->last + 2) return false;
//注意:为L->last + 2 ,因为last为最后一个元素的下标,故元素总数为L->last+1
//又我们允许用户可以把元素插入到线性表最后,于是为L->last + 2
int i;
for (i = L->last; i >= index - 1; i--) //进行位序大于等于index的元素的后移
L->data[i + 1] = L->data[i];
L->data[index - 1] = val;
L->last++;
return true;
}
(8)根据指定位序删除
bool Delete(List L, int index)
{
if (IsEmpty(L)) return false;
if (index<1 || index>L->last+1) return false;
int i;
for (i = index; i <= L->last; i++) //进行位序大于等于index+1的元素的前移
L->data[i - 1] = L->data[i];
L->last--;
return true;
}
(9)删除线性表
void Delete_List(List L)
{
if (L == NULL)
return;
free(L->data); //注意要先释放L-data对应的动态分配的空间
free(L);
return;
}
(10)顺序存储的线性表的扩容
bool Extend(List L)
{
int New_MaxSize = 2 * (L->maxsize); //进行二倍扩容
int* New_Data = (List)realloc(L->data, New_MaxSize * sizeof(int));
if (New_Data != NULL) //如果扩容成功
{
L->data = New_Data; //进行数据更新
L->maxsize = New_MaxSize;
return true;
}
else //如果扩容失败
return false;
}
(11)使用扩容之后的判满
bool IsFull_plus(List L)
{
if (L->last + 1 == L->maxsize)
{
if (Extend(L)) //它觉得自己或许还能再抢救一下....
return false;
else
return true;
}
return false;
}
(12)打印线性表
void Output(List L)
{
int i;
printf("List = [");
for (i = 0; i <= L->last; i++)
{
if(i==0)
printf("%d", L->data[i]);
else
printf(" %d", L->data[i]);
}
printf("]\n");
return;
}
主函数测试
#define maxop 20
int main()
{
srand((unsigned)time(NULL));
int val, op, index, i = 0;
List L = Init_List(15);
for (i; i < maxop; i++)
{
val = rand() % 100;
op = rand() % 4;
index = rand() % (L->last + 3) + 1;
switch (op)
{
case 0: {
printf("在位序%d处 插入元素 %d %d\n", index, val, Insert(L, index, val));
break;
}
case 1: {
printf("删除位序为%d处的元素 %d\n", index , Delete(L, index));
break;
}
case 2: {
printf("在位序%d处 插入元素 %d %d\n", index, val, Insert(L, index, val));
break;
}
case 3: {
printf("在位序%d处 插入元素 %d %d\n", index, val, Insert(L, index, val));
break;
}
}
Output(L);
printf("\n");
}
}
效果
链式存储
1.单向链表
[1]无头节点
结构定义
typedef struct Node
{
int data;
struct Node* next;
}*Position;
typedef Position List;
(1)初始化链表
List Init_List() {
return NULL;
}
(2)求表长
int Len(List L)
{
int cnt = 0; //cnt表示此时经过了几个节点
while (L)
{
L = L->next;
cnt++;
}
return cnt;
}
(3)根据指定元素查找指针(即返回该元素所在地址)
Position Find(List L, int val)
{
while (L)
{
if (L->data == val)
break;
L = L->next;
}
return L;//其实返回的有两种情况,一种为NULL,表示val在线性表中无法找到;
//一种为非NULL(即指向有效空间的指针),表示L为元素val对应的节点地址
}
(4)根据指定位序查找数据
int Find(List L, int index)
{
if(L == NULL) return ERROR; //表示此时线性表中无节点,无法进行查找
int cnt = 1; //cnt标识此时L指向的是位序为cnt的元素
while (L && cnt<index)
{
L = L->next;
cnt++;
}
if (L == NULL || cnt != index)
return NULL; //表示位序错误,无法进行查找对应位序的元素
else
return L->data; //只有当cnt==index,并且L!=NULL的时候,才表明L指向的一定为需要查找的元素所在的节点
}
(5)根据指定位序查找指针(即返回该位序对应元素所在地址)
Position Find(List L, int index)//与根据位序查找对应元素的代码基本相同,不再赘述
{
if (L == NULL) return NULL; //表示此时线性表中无节点,无法进行查找
int cnt = 1;
while (L && cnt < index)
{
L = L->next;
cnt++;
}
if (L == NULL || cnt != index)
return NULL;
else
return L;
}
(6)根据指定位序插入
List Insert(List L, int val, int index)
{
if (L == NULL)
{
L = Init_Node(val);
return L;
}
int cnt = 1; //cnt标识此时L指向的是位序为cnt的元素
Position t = NULL;
Position Front = NULL;
if (index == 1) //需要把插入位序为1的情况单独考虑
{
t = Init_Node(val);
t->next = L;
L = t;
}
else //以下为插入位序不为1情况的处理代码
{
Front = Find(L, index - 1);//调用根据位序查找指针的函数,返回位序为index-1的元素的节点地址
if (Front == NULL)
return NULL; //表示位序错误,无法进行元素插入
else
{
t = Init_Node(val);
t->next = Front->next;
Front->next = t;
}
}
return L;
}
(7)根据指定位序删除
List Delete(List L, int index)
{
if (L == NULL) return (Position)1;//表示此时线性表中无节点,无法进行删除,
//由于使用NULL进行返回,会与只有一个结点时候的情况冲突(只有一个结点的时候进行删除也是返回NULL),于是使用地址1(进行了强制类型转化)来标识无法删除
Position n = NULL;
Position Front = NULL; //指向前驱结点的指针
if (index == 1) //需要把删除位序为1的情况单独考虑
{
n = L;
L = L->next;
free(n);
}
else //以下为删除位序不为1情况的处理代码
{
Front = Find(L, index - 1);//调用根据位序查找指针的函数,返回位序为index-1的元素的节点地址
if (Front == NULL || Front->next == NULL) //注意还要排除掉该前驱结点无后继节点的情况
return (Position)1;
else
{
n = Front->next;
Front->next = Front->next->next;
free(n);
}
}
return L;
}
(8)链表删除
void Delete_List(List L)
{
Position p = L;
Position q = L;
while (q != NULL)
{
q = p->next;
free(p);
p = q;
}
return;
}
(9)打印线性表
void Output(List L)
{
Position p = NULL;
printf("List = [");
for (p = L; p != NULL; p = p->next)
{
if (p == L)
printf("%d", p->data);
else
printf(" %d", p->data);
}
printf("]\n");
}
主函数测试
#define maxop 20
int main()
{
srand((unsigned)time(NULL));
int val, op, index, i = 0;
List New_List = NULL;
List L = Init_List();
for (i; i < maxop; i++)
{
val = rand() % 100;
op = rand() % 4;
index = rand() % (Len(L) + 1) + 1;
printf("线性表长:%d\n", Len(L));
switch (op)
{
case 0: {
printf("在位序%d处 插入元素 %d\n", index, val);
L = Insert(L, val, index);
break;
}
case 1: {
printf("删除位序为%d处的元素\n", index);
New_List = Delete(L, index);
if (New_List == (Position)1)
printf("删除失败\n");
else
L = New_List;
break;
}
case 2: {
printf("在位序%d处 插入元素 %d\n", index, val);
L = Insert(L, val, index);
break;
}
case 3: {
printf("在位序%d处 插入元素 %d\n", index, val);
L = Insert(L, val, index);
break;
}
}
Output(L);
printf("\n");
}
}
效果
[2]有头节点
结构定义
typedef struct Node
{
int data;
struct Node* next;
}*PtrToNode;
typedef PtrToNode List;
typedef PtrToNode Position;
(1)初始化链表
List Init_List()
{
List L = (List)malloc(sizeof(struct Node));
L->next = NULL;
return L;
}
(2)求表长
int Len(List L)
{
int cnt = 0; //cnt标识此时已经经过了几个节点(不包括头节点)
PtrToNode p = L->next;
while (p)
{
p = p->next;
cnt++;
}
return cnt;
}
(3)根据指定元素查找指针(即返回该元素所在地址)
Position Find(List L, int val)
{
if (L->next == NULL) return NULL; //注意,判断具有头节点的线性表是否为空,需要判断L->next == NULL,而不是L == NULL
Position p = L->next; //此处必须要把真正的第一个节点的指针拿出来,如果直接使用while (L && L->data != val)
//由于L本质为一个指向头节点的指针,于是L->data的值未知,可能与val相同,从而出现意想不到的事情
while (p && p->data != val)
p = p->next;
return p;
}
(4)根据指定位序查找数据
int Find(List L, int index)
{
if (L->next == NULL) return ERROR;
int cnt = 0;
while (L && cnt < index)
{
L = L->next;
cnt++;
}
if (L == NULL || cnt != index)
return ERROR;
else//只有当L != NULL && cnt == index的时候,表示L指向的一定为位序为index的节点
return L->data;
}
(5)根据指定位序查找指针(即返回该位序对应元素所在地址)
Position Find(List L, int index)
{
if (L->next == NULL) return NULL;
int cnt = 0;
while (L && cnt < index)
{
L = L->next;
cnt++;
}
if (L == NULL || cnt != index)
return NULL;
else
return L;
}
(6)根据指定位序插入
bool Insert(List L, int index, int val)
{
int cnt = 0; //cnt标识此时L指向的是位序为cnt的元素
while (L && cnt < index - 1)
{
L = L->next;
cnt++;
}
if (L == NULL || cnt != index - 1)
return false;
else //只有当L != NULL && cnt == index-1的时候,表示L指向的一定为位序为index-1的节点
{ //为位序为index的节点的前驱节点
PtrToNode n = Init_Node(val);
n->next = L->next;
L->next = n;
return true;
}
}
(7)根据指定位序删除
bool Delete(List L, int index)
{
if (L == NULL) return false;
PtrToNode n = NULL;
Position Front = Find(L, index - 1);//调用根据位序查找指针的函数,返回位序为index-1的元素的节点地址
if (Front == NULL)
return false;
else
{
if (Front->next != NULL) //注意判断是否这个前驱节点具有后继节点(即要删除的目标节点是否存在)
{
n = Front->next;
Front->next = Front->next->next;
free(n);
return true;
}
else
return false;
}
}
(8)链表删除
void Delete_List(List L)
{
Position p = L;
Position q = L;
while (q != NULL)
{
q = p->next;
free(p);
p = q;
}
return;
}
(9)打印线性表
void Output(List L)
{
Position p = NULL;
printf("List = [");
for (p = L->next; p != NULL; p = p->next)
{
if (p == L)
printf("%d", p->data);
else
printf(" %d", p->data);
}
printf("]\n");
}
主函数测试
#define maxop 20
int main()
{
srand((unsigned)time(NULL));
int val, op, index, i = 0;
List L = Init_List();
for (i; i < maxop; i++)
{
val = rand() % 100;
op = rand() % 4;
index = rand() % (Len(L) + 1) + 1;
printf("线性表长:%d\n", Len(L));
switch (op)
{
case 0: {
printf("在位序%d处 插入元素 %d %d\n", index, val, Insert(L, index, val));
break;
}
case 1: {
printf("删除位序为%d处的元素 %d\n", index, Delete(L, index));
break;
}
case 2: {
printf("在位序%d处 插入元素 %d %d\n", index, val, Insert(L, index, val));
break;
}
case 3: {
printf("在位序%d处 插入元素 %d %d\n", index, val, Insert(L, index, val));
break;
}
}
Output(L);
printf("\n");
}
}
效果
2.循环链表(带有头节点)
结构定义
typedef struct Node
{
int data;
struct Node* next;
}*PtrToNode;
typedef PtrToNode List;
typedef PtrToNode Position;
(1)初始化链表
List Init_List()
{
List L = (List)malloc(sizeof(struct Node));
L->next = L; //带有头节点的循环链表初始化时头节点要指向自身
return L;
}
(2)求表长
int Len(List L)
{
int cnt = 0; //cnt记录p指向的是位序为cnt的结点
Position p = L;
while (p->next != L) //注意是判断是否为等于L,而不是和NULL进行比较
{
p = p->next;
cnt++;
}
return cnt; //链表中最后一个结点的位序即为整个链表的长度
}
(3)根据指定元素查找指针(即返回该元素所在地址)
Position Find(List L, int val)
{
Position p = L;
while (p->next != L && p->data != val)
p = p->next;
if (p->data == val)
return p;
else
return NULL;
}
(4)根据指定位序查找数据
int Find(List L, int index)
{
if (index < 1) return ERROR;
int i = 0;
Position p = L;
while (p->next != L && i < index)
{
p = p->next;
i++;
}
if (i == index)
return p->data;
else
return ERROR;
}
(5)根据指定位序查找指针(即返回该位序对应元素所在地址)
Position Find(List L, int index)
{
int i = 0;
Position p = L;
if (index < 1) return NULL;
while (p->next != L && i < index)
{
p = p->next;
i++;
}
if (i == index)
return p;
else
return NULL;
}
(6)根据指定位序插入
bool Insert(List L, int val, int index)
{
int i = 0;
Position n = NULL;
Position p = L;
if (index < 1) return false;
while (p->next != L && i < index - 1) //当跳出循环的时候,有两种情况:1.p->next == L,即p指向了最后一个结点
{ //2.i = index - 1,即p指向了指定位序结点的前一个结点。
//两种情况可能同时发生,即指定位序结点的前一个结点就是最后一个结点
p = p->next;
i++; //i记录p指向的是第i个位序的结点
}
if (i == index - 1) //只要p指向了指定位序结点的前一个结点,就可以进行新节点的插入操作
{
n = Init_Node(val);
n->next = p->next;
p->next = n;
return true;
}
return false;
}
(7)根据指定位序删除
bool Delete(List L, int index)
{
int i = 0;
Position p = L;
Position t = NULL;
if (index < 1) return false;
while (p->next != L && i < index - 1) //代码与上基本相同,不再赘述
{
p = p->next;
i++;
}
if (i == index - 1)
{
if (p->next != NULL)
{
t = p->next;
p->next = t->next;
free(t);
return true;
}
else
return false;
}
return false;
}
(8)链表删除
void Delete_list(List L)
{
Position p = L->next;
Position q = L->next;
while (p != L)
{
p = p->next;
free(q);
q = p;
}
free(L);
}
(9)打印线性表
void Output(List L)
{
Position p = NULL;
printf("List = [");
for (p = L->next; p != L; p = p->next)
{
if (p == L->next)
printf("%d", p->data);
else
printf(" %d", p->data);
}
printf("]\n");
}
主函数测试
#define maxop 20
int main()
{
srand((unsigned)time(NULL));
int val, op, index, i = 0;
List L = Init_List();
for (i; i < maxop; i++)
{
val = rand() % 100;
op = rand() % 4;
index = rand() % (Len(L) + 1) + 1;
printf("线性表长:%d\n", Len(L));
switch (op)
{
case 0: {
printf("在位序%d处 插入元素 %d %d\n", index, val, Insert(L, val, index));
break;
}
case 1: {
printf("删除位序为%d处的元素 %d\n", index, Delete(L, index));
break;
}
case 2: {
printf("在位序%d处 插入元素 %d %d\n", index, val, Insert(L, val, index));
break;
}
case 3: {
printf("在位序%d处 插入元素 %d %d\n", index, val, Insert(L, val, index));
break;
}
}
Output(L);
printf("\n");
}
}
效果
2、堆栈
基本介绍
描述
堆栈其实就是具有一定约束的线性表,插入和删除操作都只能在一个称为栈顶的端点位置进行。由于栈的特性,即后入栈的元素先被弹出栈,于是堆栈也被称为后入先出(Last In First Out,LIFO)表
代码实现
顺序存储
1.顺序存储
结构定义
typedef struct SNode
{
int* data;
int top; //top变量用来存储栈顶元素的【下标】,一般也称为指针,
//和C语言中的那个指针数据类型毫无关系
int max; //顺序存储数据容量
}*Stack;
(1)初始化
Stack Init_Stack(int max)
{
Stack s = (Stack)malloc(sizeof(struct SNode));
s->data = (int*)malloc(max * sizeof(int));
s->max = max;
s->top = -1; //由于初始化时没有任何元素,于是赋值为-1
return s;
}
(2)判满
bool IsFull(Stack s)
{
return s->top + 1 == s->max;
}
(3)判空
bool IsEmpty(Stack s)
{
return s->top == -1;
}
(4)入栈
bool Push(Stack s, int val)
{
if (IsFull(s)) return false;
s->top++; //将指针移到空的位置上
s->data[s->top] = val; //将元素填入
return true;
}
(5)出栈
int Pop(Stack s)
{
if (IsEmpty(s)) return ERROR;
int data = s->data[s->top]; //将栈顶元素取出
s->top--; //将指针移到新的栈顶元素上
return data;
}
(6)打印栈
void Output(Stack s)
{
int i;
printf("Stack = [栈底]");
for (i = 0; i <= s->top; i++)
printf(" %d", s->data[i]);
printf(" [栈顶]\n");
return;
}
主函数测试
#define maxop 20
int main()
{
srand((unsigned)time(NULL));
int val, op, data, i = 0;
Stack s = Init_Stack(15);
for (i; i < maxop; i++)
{
val = rand() % 100;
op = rand() % 4;
switch (op)
{
case 0: {
printf("元素%d入栈 %d\n", val, Push(s, val));
break;
}
case 1: {
data = Pop(s);
printf("栈顶元素出栈 ");
if (data == ERROR)
printf("0\n");
else
printf("1\n");
break;
}
case 2: {
printf("元素%d入栈 %d\n", val, Push(s, val));
break;
}
case 3: {
printf("元素%d入栈 %d\n", val, Push(s, val));
break;
}
}
Output(s);
printf("\n");
}
return 0;
}
效果
链式存储
2.链式存储
[1]无头节点
结构定义
typedef struct Node
{
int data;
struct Node* next;
}*PtrToNode;
typedef PtrToNode Stack;
typedef PtrToNode Position;
(1)初始化堆栈
Stack Init_Stack() {
return NULL;
}
(2)入栈
Position Push(Stack s, int val)
{
PtrToNode n = Init_Node(val);
n->next = s;
return n;
}
(3)出栈
Position Pop(Stack s)
{
if (s == NULL) return (Position)1; //由于使用NULL进行返回,会与只有一个结点时候的情况冲突(只有一个结点的时候进行删除也是返回NULL),
//于是使用地址1(进行了强制类型转化)来标识无法删除
PtrToNode n = NULL;
n = s;
s = s->next;
free(n);
return s;
}
(4)打印栈
void Output(Stack s)
{
PtrToNode p = NULL;
p = s;
printf("Stack = [栈底]");
while (p != NULL)
{
printf(" %d", p->data);
p = p->next;
}
printf(" [栈顶]\n");
return;
}
主函数测试
#define maxop 20
int main()
{
srand((unsigned)time(NULL));
int val, op, i = 0;
PtrToNode p = NULL;
Stack s = Init_Stack();
for (i; i < maxop; i++)
{
val = rand() % 100;
op = rand() % 4;
switch (op)
{
case 0: {
p = Push(s, val);
printf("元素%d入栈 ", val);
if (p != NULL)
{
printf("1\n");
s = p;
}
else
printf("0\n");
break;
}
case 1: {
p = Pop(s);
printf("栈顶元素出栈 ");
if (p != (Position)1)
{
printf("1\n");
s = p;
}
else
printf("0\n");
break;
}
case 2: {
p = Push(s, val);
printf("元素%d入栈 ", val);
if (p != NULL)
{
printf("1\n");
s = p;
}
else
printf("0\n");
break;
}
case 3: {
p = Push(s, val);
printf("元素%d入栈 ", val);
if (p != NULL)
{
printf("1\n");
s = p;
}
else
printf("0\n");
break;
}
}
Output(s);
printf("\n");
}
return 0;
}
效果
[2]有头节点
结构定义
typedef struct Node
{
int data;
struct Node* next;
}*PtrToNode;
typedef PtrToNode Stack;
typedef PtrToNode Position;
(1)初始化堆栈
Stack Init_Stack()
{
Stack s = (Stack)malloc(sizeof(struct Node));
s->next = NULL;
return s;
}
(2)入栈
bool Push(Stack s, int val)
{
PtrToNode n = Init_Node(val);
n->next = s->next;
s->next = n;
return true;
}
(3)出栈
int Pop(Stack s)
{
if (s->next == NULL) return ERROR;
PtrToNode n = NULL;
int data = s->next->data;
n = s->next;
s->next = s->next->next;
free(n);
return data;
}
(4)打印栈
void Output(Stack s)
{
PtrToNode p = NULL;
p = s->next;
printf("Stack = [栈底]");
while (p != NULL)
{
printf(" %d", p->data);
p = p->next;
}
printf(" [栈顶]\n");
return;
}
主函数测试
#define maxop 20
int main()
{
srand((unsigned)time(NULL));
int val, op, i = 0;
PtrToNode p = NULL;
Stack s = Init_Stack();
for (i; i < maxop; i++)
{
val = rand() % 100;
op = rand() % 4;
switch (op)
{
case 0: {
printf("元素%d入栈 %d\n", val, Push(s, val));
break;
}
case 1: {
printf("栈顶元素出栈 %d\n", Pop(s));
break;
}
case 2: {
printf("元素%d入栈 %d\n", val, Push(s, val));
break;
}
case 3: {
printf("元素%d入栈 %d\n", val, Push(s, val));
break;
}
}
Output(s);
printf("\n");
}
return 0;
}
效果
3、队列
基本介绍
描述
队列也是具有一定约束的线性表,插入操作只能在队尾进行,删除操作只能在队头进行,由于队列的特性,即先入队的元素先出队,于是队列也被称为先入先出(First In First Out,FIFO)表
概念
循环队列(顺序存储):
对于普通队列的“假溢出”问题,我们设计了循环队列进行解决,虽然循环队列解决了“假溢出”问题,但是对于顺序存储的方式,新的问题是无法判断队列是空还是满,于是产生了三种解决问题的方法
小贴士
队列中的max参数有哪些作用?
顺序实现的队列中的max参数,一般都是用来实现队列循环的(即通过Front和Rear移动的时候都要对max进行取余),一般情况下,max参数不是用来判断是否队列已满的(特殊情况是 使用size参数来区分队列的空、满 的时候)
Front和Rear存储的内容在不同的实现方式中有何区别?
对于顺序存储:
Front和Rear分别存储第一个元素的前一个空位置的下标,以及最后一个元素的下标
对于链式存储(无头节点):
Front和Rear分别存储第一个节点的地址,以及最后一个节点的地址
对于链式存储(有头节点):
Front和Rear分别存储头节点的地址,以及最后一个节点的地址
对于链式存储的队列,为什么是在链头进行删除,链尾进行插入?
因为链式存储的队列,具有两个指针Front和Rear,分别在链尾和链头处,如果在链尾进行删除,由前面的线性表代码可知,删除末尾的元素,需要指向倒数第二个节点的指针,但是Rear指向的是倒数第一个节点,又是单向链表,于是Rear无法移动去指向前驱节点,于是只能寄希望于Front,明显,Front距离过远,这样设计会导致效率过慢
综上所述,相比于链尾进行删除,在链头进行删除更加合理
三种实现"空","满"状态区分的方法?
1.使用记录当前存储的元素个数的参数size
根据size和max的数值比较来判断是空还是满
2.使用记录当前最后一次操作为入队还是出队的参数flag
根据flag的数值来判断是空还是满,flag每次在元素出队时会置为0,每次元素入队时会置为1
3.使用少用一个元素空间的方法
根据Front和rear的数值关系来判断是空还是满
代码实现
普通队列(非循环)链式存储
链式存储
[1]无头节点
结构定义
typedef struct Node
{
int data;
struct Node* next;
}*PtrToNode;
typedef struct QNode
{
PtrToNode Front, Rear;
}*Queue;
(1)队列的初始化
Queue Init_Queue()
{
Queue q = (Queue)malloc(sizeof(struct QNode));
q->Front = q->Rear = NULL;
return q;
}
(2)判空
bool IsEmpty(Queue q)
{
return q->Front == NULL; //两个指针皆为空(一个空另一个必也为空),表示队列空
}
(3)入队
bool Push(Queue q, int val)
{
if (IsEmpty(q)) //当队为空的时候要把两个指针都指向新节点
{
PtrToNode n = Init_Node(val);
q->Front = q->Rear = n;
return true;
}
else
{
PtrToNode n = Init_Node(val);
q->Rear->next = n;
q->Rear = n;
return true;
}
}
(4)出队
int Pop(Queue q)
{
int data;
if (IsEmpty(q)) return ERROR; //当队为空时,无法实现元素出队
if (q->Front == q->Rear) //当队中只有一个元素时,该元素出队的同时,要把两个指针置为NULL
{
data = q->Front->data;
free(q->Front);
q->Front = q->Rear = NULL;
return data;
}
else
{
data = q->Front->data;
PtrToNode n = q->Front;
q->Front = q->Front->next;
free(n);
return data;
}
}
(5)打印队列
void Output(Queue q)
{
PtrToNode p = NULL;
p = q->Front;
printf("Queue = [队头]");
while (p != NULL)
{
printf(" %d", p->data);
p = p->next;
}
printf(" [队尾]\n");
return;
}
主函数测试
#define maxop 20
int main()
{
srand((unsigned)time(NULL));
int val, op, data, i = 0;
PtrToNode p = NULL;
Queue q = Init_Queue();
for (i; i < maxop; i++)
{
val = rand() % 100;
op = rand() % 4;
switch (op)
{
case 0: {
printf("元素%d入队 %d\n", val, Push(q, val));
break;
}
case 1: {
data = Pop(q);
printf("栈顶元素出队 ");
if (data != ERROR)
printf("1\n");
else
printf("0\n");
break;
}
case 2: {
printf("元素%d入队 %d\n", val, Push(q, val));
break;
}
case 3: {
printf("元素%d入队 %d\n", val, Push(q, val));
break;
}
}
Output(q);
printf("\n");
}
return 0;
}
效果
[2]有头节点
结构定义
typedef struct Node
{
int data;
struct Node* next;
}*PtrToNode;
typedef struct QNode
{
PtrToNode Front, Rear;
}*Queue;
(1)队列的初始化
Queue Init_Queue()
{
Queue q = (Queue)malloc(sizeof(struct QNode));
q->Front = q->Rear = (PtrToNode)malloc(sizeof(struct Node));
q->Rear->next = NULL;
return q;
}
(2)判空
bool IsEmpty(Queue q)
{
return q->Front == q->Rear; //只有当两个指针都指向头节点的时候,队列才为空
}
(3)入队
bool Push(Queue q, int val)
{
PtrToNode n = Init_Node(val);
q->Rear->next = n;
q->Rear = n;
return true;
}
(4)出队
int Pop(Queue q)
{
if (IsEmpty(q)) return ERROR;
int data;
PtrToNode n = q->Front->next;
if (q->Rear == n) //当队中只有一个元素的时候,该元素出队的同时,要把Rear赋值Front,使两个指针都指向头节点
{
data = q->Front->next->data;
free(n);
q->Rear = q->Front;
q->Front->next = NULL; //记得要把头节点的指针置为NULL,
//否则打印的时候会发生非法访问以及动态释放的空间
return data;
}
else
{
data = q->Front->next->data;
q->Front->next = q->Front->next->next;
free(n);
return data;
}
}
(5)打印队列
void Output(Queue q)
{
PtrToNode p = NULL;
p = q->Front->next;
printf("Queue = [队头]");
while (p != NULL)
{
printf(" %d", p->data);
p = p->next;
}
printf(" [队尾]\n");
return;
}
主函数测试
#define maxop 20
int main()
{
srand((unsigned)time(NULL));
int val, op, data, i = 0;
PtrToNode p = NULL;
Queue q = Init_Queue();
for (i; i < maxop; i++)
{
val = rand() % 100;
op = rand() % 4;
switch (op)
{
case 0: {
printf("元素%d入队 %d\n", val, Push(q, val));
break;
}
case 1: {
data = Pop(q);
printf("元素出队 ");
if (data != ERROR)
printf("1\n");
else
printf("0\n");
break;
}
case 2: {
printf("元素%d入队 %d\n", val, Push(q, val));
break;
}
case 3: {
printf("元素%d入队 %d\n", val, Push(q, val));
break;
}
}
Output(q);
printf("\n");
}
return 0;
}
效果
循环队列顺序存储
顺序存储
三种实现"空","满"状态区分的方法:
1.使用记录当前存储的元素个数的参数size
结构定义
typedef struct QNode
{
int* data;
int Front, Rear;
int max;
int size;
}*Queue;
(1)初始化
Queue Init_Queue(int max)
{
Queue q = (Queue)malloc(sizeof(struct QNode));
q->data = (int*)malloc(max * sizeof(int));
q->Front = q->Rear = 0;
q->size = 0;
q->max = max;
return q;
}
(2)判空
bool IsEmpty(Queue q)
{
return q->size == 0;
}
(3)判满
bool IsFull(Queue q)
{
return q->size == q->max;
}
(4)入队
bool Push(Queue q, int val)
{
if (IsFull(q)) return false;
q->Rear = (q->Rear + 1) % q->max;
q->data[q->Rear] = val;
q->size++;
return true;
}
(5)出队
int Pop(Queue q)
{
if (IsEmpty(q)) return ERROR;
int data;
q->Front = (q->Front + 1) % q->max;
data = q->data[q->Front];
q->size--;
return data;
}
(6)打印队列
void Output(Queue q)
{
int i, j = q->Front + 1;
int n = q->size;
printf("Queue = [队头]");
for (i = 0; i < n; i++)
{
printf(" %d", q->data[j++ % q->max]);
}
printf(" [队尾]\n");
return;
}
效果(由于三种方法可以共用一个测试主函数,于是我将主函数代码放置在最后)
2.使用记录当前最后一次操作为入队还是出队的参数flag
结构定义
typedef struct QNode
{
int* data;
int Front, Rear;
int max;
int flag;
}*Queue;
(1)初始化
Queue Init_Queue(int max)
{
Queue q = (Queue)malloc(sizeof(struct QNode));
q->data = (int*)malloc(max * sizeof(int));
q->Front = q->Rear = 0;
q->max = max;
q->flag = 0;
return q;
}
(2)判空
bool IsEmpty(Queue q)
{
if (q->Front == q->Rear && q->flag == 0)
return true;
else
return false;
}
(3)判满
bool IsFull(Queue q)
{
if (q->Front == q->Rear && q->flag == 1)
return true;
else
return false;
}
(4)入队
bool Push(Queue q, int val)
{
if (IsFull(q)) return false;
q->Rear = (q->Rear++) % q->max;
q->data[q->Rear] = val;
q->flag = 1;
return true;
}
(5)出队
int Pop(Queue q)
{
if (IsEmpty(q)) return ERROR;
q->Front = (q->Front + 1) % q->max;
q->flag = 0;
return q->data[q->Front];
}
(6)打印队列
void Output(Queue q)
{
int i = q->Front + 1;
printf("Queue = [队头]");
if (IsEmpty(q) != true)
{
while (i != q->Rear + 1)
printf(" %d", q->data[i++ % q->max]);
}
printf(" [队尾]\n");
return;
}
效果(由于三种方法可以共用一个测试主函数,于是我将主函数代码放置在最后)
3.使用少用一个元素空间的方法
结构定义
typedef struct QNode
{
int* data;
int Rear, Front;
int max;
}*Queue;
(1)初始化
Queue Init_Queue(int max)
{
Queue q = (Queue)malloc(sizeof(struct QNode));
q->data = (int*)malloc(max * sizeof(int));
q->Front = q->Rear = 0; //Front和Rear依旧存储的是第一个元素的前一个位置的下标,以及最后一个元素的下标,
//初始化时二者全部赋值为0表示初始第一个元素的位置是不放置元素的,
//当然,随着插入和删除操作的进行,不放元素的位置的可能会发生变动
q->max = max;
return q;
}
(2)判空
bool IsEmpty(Queue q)
{
return q->Rear == q->Front;
}
(3)判满
bool IsFull(Queue q)
{
return (q->Rear + 1) % q->max == q->Front; //判满函数的具体实现是两个指针在队列满的时候,
//不会像队列空的时候那样指向同一个元素的原因
}
(4)入队
bool Push(Queue q, int val)
{
if (IsFull(q)) return false;
q->Rear = (q->Rear + 1) % q->max;
q->data[q->Rear] = val;
return true;
}
(5)出队
int Pop(Queue q)
{
if (IsEmpty(q)) return ERROR;
int data;
q->Front = (q->Front + 1) % q->max;
data = q->data[q->Front];
return data;
}
(6)打印队列
void Output(Queue q)
{
int i = q->Front + 1;
printf("Queue = [队头]");
if (IsEmpty(q) != true)
{
while (i != q->Rear + 1)
printf(" %d", q->data[i++ % q->max]);
}
printf(" [队尾]\n");
return;
}
效果(由于三种方法可以共用一个测试主函数,于是我将主函数代码放置在最后)
顺序存储的循环队列三种方法的测试主函数
#define maxop 20
int main()
{
srand((unsigned)time(NULL));
int val, op, data, i = 0;
Queue q = Init_Queue(15);
for (i; i < maxop; i++)
{
val = rand() % 100;
op = rand() % 4;
switch (op)
{
case 0: {
printf("元素%d入队 %d\n", val, Push(q, val));
break;
}
case 1: {
data = Pop(q);
printf("元素出队 ");
if (data != ERROR)
printf("1\n");
else
printf("0\n");
break;
}
case 2: {
printf("元素%d入队 %d\n", val, Push(q, val));
break;
}
case 3: {
printf("元素%d入队 %d\n", val, Push(q, val));
break;
}
}
Output(q);
printf("\n");
}
return 0;
}
来自作者的话:
此文章的内容还在逐步修进中,希望各位读者可以不吝赐教,有问题都可以在评论区提出来,我看到了会尽快进行更改
于 2022/12/4 第一次整改完毕