前言
对于栈和队列都是基于线性表实现的一种数据结构,本身就是一种对线性表的延申应用,而形成的新的存储结构,所以朋友们请不要害怕,这两个数据结构本身非常简单,相信你们看了本篇文章也一定为这样认为。
一、栈
1. 定义
栈是限定在栈顶进行插入或删除操作的线性表,相对应的另一端就被称为栈底。在没有存储任何数据时,被称为空栈。
也就是我们存数据操作被称为压栈,取出数据被称为出栈,因为基于从栈顶的原因,所以栈的结构被我们称为LIFO(Last In First Out)。
栈顶的表示有两种方法,一种是栈顶的下一个数据为栈的最上层数据,另一种是栈顶就代表栈内最顶点元素。二者没有本质上的差别,数据结构也没有具体规定,只是实现方法上有所不同,本文章就采用第一种方法实现。
2. 代码实现
栈的基本操作应该包含插入,删除,初始化,判空,获取栈顶元素,获取栈内元素个数,以及销毁功能。
栈的结构组成:
因为我们栈的实现是利用顺序表实现,所以本质上与顺序表没有差别,只有原本顺序表中的大小,变为了top表示。栈本身能够存储的数据个数不确定,所以加上了动态空间和一个表示容量的变量。
typedef int ElementType;
typedef struct Stack
{
ElementType* arr;
int capacity;
int top;
}Stack;
初始化:
因为我们开辟的空间是在结构体内部,而传入的结构体指针不可能为空,所以我们必须防止进入函数的结构体指针为空。有的伙伴可能有一点疑惑,不可能为空还判断它干嘛,事实上,如果是面对其他人使用,他们不知道这一规则,那么他们就有可能传入空指针,所以需要避免。
//初始化
void StackInit(Stack* ps)
{
assert(ps);
ps->arr = (ElementType*)malloc(sizeof(ElementType)* 4);
assert(ps->arr);
ps->top = 0;
ps->capacity = 4;
}
压栈:
考虑到整个栈项目功能只有压栈操作有可能需要扩容,所以我们不需要封装一个扩容函数,扩容逻辑也很简单,只要我的栈顶与容量相等,就进行扩容。
然后对栈顶位置赋值,再对栈顶加一,用于下一次压栈。如果这里用的是栈顶表示栈顶元素本身,那么就需要先加一再赋值。
//压栈
void StackPush(Stack* ps, ElementType data)
{
assert(ps);
if (ps->top == ps->capacity)
{
ElementType* temp = realloc(ps->arr, sizeof(ElementType)*ps->capacity * 2);
assert(temp);
ps->arr = temp;
ps->capacity *= 2;
}
ps->arr[ps->top] = data;
ps->top++;
}
出栈:
出栈唯一需要考虑的就是当栈已经为空栈了就不能再次删除,需要对此作出反应。如果还有数据,那么就将栈顶减一。不删除原位置数据的原因是因为那个位置按照栈的逻辑我们已经不能够再次使用了,变化数据没有实际意义。
//出栈
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
ps->top--;
}
获取栈顶元素、获取有效个数、判空:
这三个功能我就不讲解了,只有一句代码,相信你们也能理解。这里我就讲一下为什么一句代码也需要封装函数的问题。
我们封装函数的原因不是因为他是否好实现,而是我们需要做一个对外的接口,让其余使用者能够清晰的得到他想要知道的数据。作为项目的实现者,我们知道该栈的结构如何,是按照何种逻辑实现,但是对于其他人就不是这样的。比如说,我不告诉你我的栈顶是从0计数还是-1开始计数,你能知道栈顶真正代表的位置吗?当然并不是没有办法知道,只需要调试几下就能得到答案。
但是如果这是按照C++书写的代码,用户是不可能访问我的结构的,他永远不可能得到内部实际内容,这才是最主要的原因。
//获取栈顶元素
ElementType StackTop(Stack* ps)
{
return ps->arr[ps->top - 1];
}
//获取有效个数
int StackSize(Stack* ps)
{
return ps->top;
}
//判断是否为空
bool StackEmpty(Stack* ps)
{
return ps->top == 0;
}
销毁:
作为在堆上开辟的空间,结构没用时就需要对其进行释放内存。
//销毁
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->top = 0;
ps->capacity = 0;
}
二、队列
1. 定义
和栈相反,队列是一种先进先出(First In First Out)的线性表,他只允许在表的一端插入,另一端删除元素。就和我们生活当中的排队是相同逻辑。允许插入数据的一端被称为队尾,允许删除的一端被称为队头。
2. 代码实现
与栈相同,队列功能为初始化、插入、删除、获取队头元素、获取队尾元素、获取有效个数、判空、销毁功能。
结构组成:
队列的实现基于链表,因为通过顺序表有些功能会显得很挫,很笨重,利用一个一个的结点刚好能够实现。
对于由链表实现的队列,我们想要能直接知道队头和队尾,那就需要两个指针分别指向这两个位置,利用size变量记录数据个数。注意两个结构体分别代表队列的结构和队列本身。
typedef int QElementType;
typedef struct QListNode
{
QElementType data;
struct QListNode* next;
}QNode;
typedef struct QueueNode
{
QNode* _Front;
QNode* _Rear;
int size;
}Queue;
初始化:
队列的初始化不像栈,因为结构本身我们定义时就已经产生,所以只需要对内容设置起始值就行,作为空列表,内部没有数据,那么两个指针都应该指向空。
//初始化
void QueueInit(Queue* pNode)
{
assert(pNode);
pNode->_Front = NULL;
pNode->_Rear = NULL;
pNode->size = 0;
}
入列:
队列的实现结构是链表,所以添加数据必定需要创造结点,这里的实现我分为了两种情况,当队列为空时,两个指针都指向同一块空间,此后添加数据就只需要尾插_Rear队尾指针就行。
//队列尾插
void QueuePush(Queue* pNode, QElementType datum)
{
assert(pNode);
//创建结点
QNode* NewNode = (QNode*)malloc(sizeof(QNode));
assert(NewNode);
NewNode->data = datum;;
NewNode->next = NULL;
//队列没有元素时
if (QueueEmpty(pNode))
{
pNode->_Front = NewNode;
pNode->_Rear = NewNode;
}
//有元素时
else
{
pNode->_Rear->next = NewNode;
pNode->_Rear = pNode->_Rear->next;
}
pNode->size++;
}
出列:
同样的,出列也被分为两种情况,当数据只剩最后一个时,删除之后需要将两个指针指向空,防止非法访问。当数据大于1个时,直接头删就行。其中当队列为空时,已经不能被删除,这时就需要提示。
//队列头删
void QueuePop(Queue* pNode)
{
assert(pNode);
//是否还能删除
assert(!QueueEmpty(pNode));
//删到最后一个结点时
if (pNode->_Front == pNode->_Rear)
{
free(pNode->_Front);
pNode->_Front = NULL;
pNode->_Rear = NULL;
}
//还有多个结点时
else
{
QNode* temp = pNode->_Front;
pNode->_Front = pNode->_Front->next;
free(temp);
}
pNode->size--;
}
获取队头、获取队尾、获取有效个数、判空;
因为我们的队列结构有这几个功能的变量,直接返回即可。我们加入size变量的目的就是为了保持项目功能的实现一致,让其时间复杂度变为O(1)。
//获取队头
QElementType QueueFront(Queue* pNode)
{
assert(pNode);
if (QueueEmpty(pNode)) return -1;
return pNode->_Front->data;
}
//获取队尾
QElementType QueueBack(Queue* pNode)
{
assert(pNode);
if (QueueEmpty(pNode)) return -1;
return pNode->_Rear->data;
}
//获取有效个数
int QueueSize(Queue* pNode)
{
assert(pNode);
return pNode->size;
}
//判断是否为空
int QueueEmpty(Queue* pNode)
{
assert(pNode);
return pNode->size == 0;
}
销毁:
因为实现方式为链表,所以一定需要每个结点单独删除,直到结点被删完。
//销毁
void QueueDestroy(Queue* pNode)
{
assert(pNode);
while (pNode->size != 0)
{
QNode* temp = pNode->_Front;
pNode->_Front = pNode->_Front->next;
free(temp);
pNode->size--;
}
pNode->_Front = NULL;
pNode->_Rear = NULL;
}
三、循环队列
1. 定义
循环队列就是队列,只不过它新添加了一部分功能。大家直到旋转餐厅吧,餐厅的传送带大小被固定,若是食物已经被放满了,厨师就不能放置了,当传送带上没有食物,消费者也不能够吃东西,同样的消费和放置的方法也是队头出、队尾加。
2. 代码实现
结构组成:
按照我最开始的想法,利用链表实现该功能。逻辑上是很简单的,但是实际实现起来就会有很多的问题产生,比如如何判断队列为空,怎么才算队列为满,如何判断队头和队尾之间相差几个元素,由此的诸多原因,让我决定使用顺序表实现该功能。
typedef int QElementType;
typedef struct
{
QElementType* arr;
int front;
int rear;
int nums;
} MyCircularQueue;
实现的底层思想:
在规定的大小后面再添加一片空间,用于判断空间是否为空或者为满,当头指针和尾指针指向相同位置表示为空,当尾指针加一等于头指针,表示队列已满。
初始化:
给定开辟k大小的空间,但是我们多建立一个空间。
//初始化
MyCircularQueue* myCircularQueueCreate(int k)
{
MyCircularQueue* List = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
//多创建一个空间,用于判空判满
List->arr = (QElementType*)malloc(sizeof(QElementType)*(k + 1));
//头
List->front = 0;
//尾
List->rear = 0;
//空间大小
List->nums = k + 1;
return List;
}
入列:
因为该队列为循环队列,所以添加位置在空间之后需要重新指向开头位置,也就是说,这个地方利用取余思想很完美,当rear+1 % 空间大小,就能得到应该添加的位置下标。例如空间大小为4,rear为3(已经指向结构最后一个数了),3+1 % 4 = 0。
其次判断是否添加也是同样的道理,当front等于0,rear+1 % nums也为0,表示空间已满,不能再次添加。
//插入数据
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
assert(obj);
//当结构没有满
if ((obj->rear + 1) % obj->nums != obj->front)
{
obj->arr[obj->rear] = value;
obj->rear++;
obj->rear %= obj->nums;
return true;
}
return false;
}
出列:
出列只用判断队列是否为空就行,而为空条件就是front和rear相等。
//删除数据
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
assert(obj);
//当还有数据
if (obj->front != obj->rear)
{
obj->front++;
obj->front %= obj->nums;
return true;
}
return false;
}
队头元素:
当队列不为空,返回front对应下标的数据即可。为空时返回-1表示。
//队头元素
int myCircularQueueFront(MyCircularQueue* obj)
{
assert(obj);
if (obj->front == obj->rear) return -1;
return obj->arr[obj->front];
}
队尾元素:
实际上我们的rear的下标为最后一个数据的下一个下标,也就是我们在得知rear时,去最后一个数据需要找到它的前一个元素。
//队尾元素
int myCircularQueueRear(MyCircularQueue* obj)
{
int temp = obj->rear;
if (obj->front == obj->rear) return -1;
//当尾指针指向开头时,让其指向最后一个空间
if ((temp - 1) == -1) temp = obj->nums;
return obj->arr[temp - 1];
}
判空、判满、销毁
//判空
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
assert(obj);
//相等为空
if (obj->front == obj->rear) return true;
return false;
}
//判满
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
assert(obj);
//尾加一等于头为满
if (((obj->rear + 1) % obj->nums) == obj->front) return true;
return false;
}
//销毁
void myCircularQueueFree(MyCircularQueue* obj)
{
assert(obj);
free(obj->arr);
free(obj);
}
总结
以上就是全部内容了,希望能帮到你们,作为一个小萌新的祝愿,欸嘿。