🏠关于此专栏:Super数据结构专栏将使用C/C++语言介绍顺序表、链表、栈、队列等数据结构,每篇博文会使用尽可能多的代码片段+图片的方式。
🚪归属专栏:Super数据结构
🎯每日努力一点点,技术累计看得见
文章目录
- 栈
- 栈的概念和结构
- 栈的实现
- 队列
- 队列的概念及结构
- 队列的实现
- 环形队列
- 综合巩固
栈
老式的手电筒是装电池的,如下图所示。1号电池装入后,再装第2号电池。如果想从中取出电池,只有先取出第2号电池,才能取第1号电池。像这种后进入先出来的结构,在数据结构中被称为栈。
栈的概念和结构
栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
栈有两种常用的操作,从栈顶取数据以及将输入放入栈中。对于这两种操作,我们经常将它们称为出栈和压栈。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
栈的实现
栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入、删除数据的代价比较小。下面使用数组实现栈结构。
在开始实现栈结构之前,我们要先定义一个栈结构。我们需要一个动态开辟的数组,用它存储栈内元素,一个变量记录当前栈顶的位置,还需要一个变量记录栈当前的容量。(这里的栈顶指针top指向栈顶元素的下一位置)
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}Stack;
下面给出栈的各个接口↓↓↓
//初始化
void StackInit(Stack* s, int n);
//销毁
void StackDestory(Stack* s);
//入栈
void StackPush(Stack* s, STDataType x);
//出栈
void StackPop(Stack* s);
//获取栈顶元素
STDataType StackTop(Stack* s);
//判断栈是否为空
bool IsEmpty(Stack* s);
//获取栈已保存元素个数
int StackSize(Stack* s);
栈的初始化
首先给出栈的初始化操作,根据用户给出的初始容量n,为动态数组a开辟n个空间,并将capacity改为n。由于初始时没有保存任何元素,故将栈顶指针top指向0。
void StackInit(Stack* s, int n)
{
assert(s);
s->a = (STDataType*)malloc(sizeof(STDataType) * n);
s->top = 0;
s->capacity = n;
}
栈的销毁
栈在堆区开辟空间,栈不再使用时,需要将开辟的空间释放掉。再将top指针和容量置为0。
void StackDestory(Stack* s)
{
assert(s);
free(s->a);
s->capacity = s->top = 0;
}
入栈操作
在入栈前,需要检查容量是否足够,如果容量不足,则扩容2倍空间。由于栈顶指针top指向的是栈顶元素的下一个位置,所以我们要先在top下标位置保存入栈元素,再将top指针加1。
void StackPush(Stack* s, STDataType x)
{
assert(s);
if (s->top == s->capacity)
{
s->a = (STDataType*)realloc(s->a, sizeof(STDataType) * s->capacity * 2);
if (s->a == NULL)
{
perror("malloc error!\n");
return 0;
}
s->capacity *= 2;
}
s->a[s->top++] = x;
}
出栈操作
如果栈中已经没有元素,此时应该禁止用户出栈。如果当前栈中有一个以上元素,我们只需要将栈顶指针top减一,即可完成出栈操作。
例如:有一个保存了4个元素的栈,初始时top指向栈顶元素的下一位置,如下图绿色箭头所示。当执行出栈操作后,top减1,top直向出栈前的栈顶元素。由于栈顶有效元素位置位于top指针以下,故此时红色top指针指向的数字4不是有效元素。
void StackPop(Stack* s)
{
assert(s);
assert(!IsEmpty(s));
--s->top;
}
获取栈顶元素
由于栈顶指针top指向栈顶元素的下一个位置,我们在返回栈顶元素时,需要返回下标为top-1的元素。注意:在栈为空时,不允许用户获取栈顶元素。
STDataType StackTop(Stack* s)
{
assert(s);
assert(!IsEmpty(s));
return s->a[s->top - 1];
}
判断栈是否为空 及 获取栈已存储元素数
由于栈顶指针top保存栈顶元素的下一个下标。因而,没有元素时它保存的值就是0,有元素时它就是栈中已存储的元素数。
bool IsEmpty(Stack* s)
{
assert(s);
return s->top == 0;
}
int StackSize(Stack* s)
{
assert(s);
return s->top;
}
队列
我们去奶茶店买奶茶时,需要按先来先点餐/取餐的方式。类似的场景包括医院叫号、银行叫号等等。生活中存在着大量的先到先服务的场景,我们将这种排队结构称之为队列。
队列的概念及结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 。
入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
队列的实现
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
在开始实现之前,我们一样定义一个队列的存储结构。由于这里使用单链表结构实现队列,所以需要先创建链表结点结构QNode。队列需要保存头和尾,因而在Queue结构中需要存储头尾结点。
typedef int QDataType;
typedef struct QueueNode
{
QDataType val;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
}Queue;
接下来一起看看队列的各个接口↓↓↓
void QueueInit(Queue* q);
void QueueDestory(Queue* q);
void QueuePush(Queue* q, QDataType x);
void QueuePop(Queue* q);
QDataType QueueFront(Queue* q);
QDataType QueueBack(Queue* q);
int QueueSize(Queue* q);
bool IsEmpty(Queue* q);
队列初始化
队列初始时没有任何元素,我们应该让它的头尾指针均指向空。
void QueueInit(Queue* q)
{
assert(q);
q->head = q->tail = NULL;
}
队列的销毁
头结点指向第一个元素的结点,我通过用next指针保存头结点的下一个结点,释放完头指针指向的结点后,再让它等于next指针,next指针再指向头指针的下一个结点…(以此类推)。当头指针为空时,整个队列就销毁完成了。注意,尾指针还不是空,还需要将它置空。
void QueueDestory(Queue* q)
{
assert(q);
while (q->head != NULL)
{
QNode* next = q->head->next;
free(q->head);
q->head = next;
}
q->tail = NULL;
}
入队操作
在队列中新增元素,只能在队尾添加。当队列还没有任何元素时,我们需要创建一个结点保存新元素,并将头尾指针指向它;如果队列中已经有元素时,我们只需要将尾结点的next域改为新结点,并将尾指针指向新添加的结点即可。
void QueuePush(Queue* q, QDataType x)
{
assert(q);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
newnode->val = x;
newnode->next = NULL;
if (q->head == NULL)
{
q->head = q->tail = newnode;
}
else
{
q->tail->next = newnode;
q->tail = newnode;
}
}
出队操作
如果队列中没有任何元素,则不允许用户出队。如果此时队列中仅有一个元素,则将它出队后,需要将头尾指针置空;如果队列中不止一个元素,则要先保存头指针的下一个结点,在将头结点删除后,将头指针改为新的头结点。
void QueuePop(Queue* q)
{
assert(q);
assert(!IsEmpty(q));
if (q->head == q->tail)
{
free(q->head);
q->head = q->tail = NULL;
}
else
{
QNode* newHead = q->head->next;
free(q->head);
q->head = newHead;
}
}
获取头部元素
头指针指向的就是第一个结点的地址,只要返回它指向的结点的val域即可。
QDataType QueueFront(Queue* q)
{
assert(q);
assert(!IsEmpty(q));
return q->head->val;
}
获取尾部元素
尾指针指向的就是最后一个结点的地址,只要返回它指向的结点的val域即可。
QDataType QueueBack(Queue* q)
{
assert(q);
assert(!IsEmpty(q));
return q->tail->val;
}
获取队列元素数量
使用一个新指针保存头指针保存的结点地址。通过不断向后移动,每次移动就记录一次长度,当走到空时,就将整个队列走完了。此时返回记录的长度即可。
int QueueSize(Queue* q)
{
assert(q);
int size = 0;
QNode* cur = q->head;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}
判断队列是否为空
如果头结点指针指向空,就说明整个队列为空。
bool IsEmpty(Queue* q)
{
assert(q);
return q->head == NULL;
}
环形队列
上面实现的队列使用的是链表。假如我们使用的是数组存储队列那会是什么效果呢?
如果我们用一个长度为长度为8的数组存储队列元素。使用front存储队列开始的下标,rear指向队尾元素的下一个下标。如果元素入队列,则Queue[rear]=新元素,在++rear。如果元素出队列则让front++即可。
但如果不断入队不断出队,最终rear和front都会超出数组下标范围。此时我们通过在将rear++和front操作改为(rear+1)%len及(front+1)%len[其中len是数组长度],就可以让rear或front回到数组最前面。这样就可实现数组空间重复利用。
但front==rear
时我们已经定义为NULL,队列中没有元素。如果我们不断插入元素,则在插入8个元素后,rear和front重合,这时候就引出一个问题:rear==front
到底是表示空,还是表示满呢?
为了解决这个问题,我们可以选择在front前面的一个元素不保存数据,使用(rear + 1) % len == front
来作为队列已经满的依据,使用rear == front
作为队列为空的依据。
这时计算队列的长度可以使用(rear - front + len) % len
来计算。对于循环队列,这里只做介绍,不进行代码演示了。
综合巩固
学完上面的内容,下面我们来牛刀小试以下:
( ఠൠఠ )ノtest1:有效的括号
这里使用C语言时,需要将我们上述实现的栈结构全部包含进来。我了展示方便,这里给出的代码是C++语言。如果进入的是左括号,则直接保存到栈中;如果是右括号,则与栈中的括号匹配,不符合则返回false,如果匹配成功,则将栈顶弹出(因为这对括号已经能配对了)。直到s字符串走到结束时,这时候需要查看栈中是否还有括号,如果还有,说明这个字符串中的左括号多余右括号,故返回false;如果没有括号了,则说明所有括号都配对成功。
class Solution {
public:
bool isValid(string s) {
stack<char>stk;
for(auto e : s)
{
if(!stk.empty() &&(
(e == ')' && stk.top() == '(') ||
(e == ']' && stk.top() == '[') ||
(e == '}' && stk.top() == '{')
))
{
stk.pop();
}
else if(e == '(' || e == '[' || e == '{')
{
stk.push(e);
}
else
{
return false;
}
}
return stk.empty();
}
};
( ఠൠఠ )ノtest2:用队列实现栈
所有入栈的内容都保存在q1队列中,当需要获取栈顶元素时,则获取q1的队尾元素;当需要弹出栈顶元素,先将q1元素入到q2,直到只剩一个元素,这个元素用做返回,在返回前,将q2的内容再导会q1中。q2这里的作用仅仅是在q1需要做出栈时,作为q1队尾之前的各个元素保存的临时队列。
class MyStack {
public:
MyStack() {
}
void push(int x) {
q1.push(x);
}
int pop() {
while(q1.size() > 1)
{
q2.push(q1.front());
q1.pop();
}
int ret = q1.front();
q1.pop();
while(!q2.empty())
{
q1.push(q2.front());
q2.pop();
}
return ret;
}
int top() {
return q1.back();
}
bool empty() {
return q1.empty();
}
queue<int>q1;//用于保存数据
queue<int>q2;//用于临时交换
};
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/
( ఠൠఠ )ノtest3:用栈实现队列
使用一个输出栈和一个输入栈。当入队是元素均入到输入栈中;当输出栈为空时,将输入栈的元素全部入到输出栈,这样可以使得本来位于输入栈栈底的元素跑到输出栈的栈顶,这么操作就可以实现先进先出。
class MyQueue {
public:
MyQueue() {
}
void push(int x) {
pushstack.push(x);
}
int pop() {
if(popstack.empty())
{
while(!pushstack.empty())
{
popstack.push(pushstack.top());
pushstack.pop();
}
}
int ret = popstack.top();
popstack.pop();
return ret;
}
int peek() {
if(popstack.empty())
{
while(!pushstack.empty())
{
popstack.push(pushstack.top());
pushstack.pop();
}
}
return popstack.top();
}
bool empty() {
return popstack.empty() && pushstack.empty();
}
stack<int>popstack;
stack<int>pushstack;
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
( ఠൠఠ )ノtest4:设计循环队列
这道题是对上面介绍的循环队列的实现。涉及下标的问题,多使用模运算符,实现下标从数组尾到头和头到尾的转换。
class MyCircularQueue {
public:
MyCircularQueue(int k) {
len = k + 1;
a = (int*)malloc(sizeof(int) * len);
front = rear = 0;
}
bool enQueue(int value) {
if((rear + 1) % len == front) return false;
a[rear] = value;
rear = (rear + 1) % len;
return true;
}
bool deQueue() {
if(rear == front) return false;
front = (front + 1) % len;
return true;
}
int Front() {
if(isEmpty()) return -1;
return a[front];
}
int Rear() {
if(isEmpty()) return -1;
return a[(rear - 1 + len) % len];
}
bool isEmpty() {
return front == rear;
}
bool isFull() {
return (rear + 1) % len == front;
}
int* a;
int front;
int rear;
int len;
};
/**
* Your MyCircularQueue object will be instantiated and called as such:
* MyCircularQueue* obj = new MyCircularQueue(k);
* bool param_1 = obj->enQueue(value);
* bool param_2 = obj->deQueue();
* int param_3 = obj->Front();
* int param_4 = obj->Rear();
* bool param_5 = obj->isEmpty();
* bool param_6 = obj->isFull();
*/
文章结语:这篇文章对时间复杂度、空间复杂度、数据结构与算法概念进行了简要的介绍。
🎈欢迎进入Super数据结构专栏,查看更多文章。
如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d