栈的定义
栈(Stack)是一种数据结构,它按照后进先出(LIFO,Last In First Out)的原则进行操作。栈可以看作是一种特殊的线性表,只能在表的一端进行插入和删除操作,这一端称为栈顶,另一端称为栈底。
栈的定义包括以下要素:
数据元素:栈中存储的数据元素可以是任意类型的对象,可以是基本数据类型、结构体、类等。
栈顶指针:栈顶指针指向栈顶元素的位置。当栈为空时,栈顶指针通常指向一个特定的空值。
压栈(Push)操作:将一个新的元素添加到栈顶,栈顶指针向上移动。
弹栈(Pop)操作:从栈顶移除一个元素,栈顶指针向下移动。
栈空判断:判断栈是否为空,即栈中是否没有元素。
栈满判断:在使用静态数组实现栈时,判断栈是否已满,即栈中元素数量是否达到了数组的容量上限。
栈的本质上认仍然是一个线性表,只是比较特殊,它满足线性表的所有性质,但被施加限制,只允许在表尾进行插入和删除操作(这里的表尾指的是栈顶);
简而言之,栈是一个先进先出,栈底不变,只允许在栈顶进行插入和删除的线性表 ;
栈分为两种: 顺序栈和链栈
顺序栈
顺序栈是一种以数组实现栈的一种方式,与用单链表形式实现栈的链栈不同,顺序栈往往占用较少的空间(因为链栈需要申请很多个指针地址),但也伴随着空间不足造成越界的危险 ;
定义:
typedef struct {
int stack[100] ;
int top ;
} Sqstack;
初始化:
Sqstack* stack = (Sqstack*) malloc(sizeof(Sqstack)) ;
stack->top = -1 ;//初始化
压栈:
int push (Sqstack* s, int e) {
if (s->top >= 99) {
printf("栈满,push失败") ;
return 0;
} else {
s->stack[++s->top] = e ;
return 1;
}
}
弹栈:
int pop (Sqstack* s, int *e) {
if (s->top == -1) {
printf("栈空, pop失败") ;
return 0;
} else {
*e = s->stack[s->top] ;
s->top-- ;
return 1;
}
}
在压栈和弹栈中,顺序表要注意最大限制,这就涉及到了栈的溢出和下溢 ;
栈的溢出(Stack Overflow):当尝试将元素压入栈时,如果栈已满,则会发生栈溢出。这可能会导致程序崩溃或出现不可预料的行为。因此,在压栈操作之前,应该先检查栈是否已满,以避免溢出。
栈的下溢(Stack Underflow):当尝试从栈中弹出元素时,如果栈为空,则会发生栈下溢。这意味着没有元素可供弹出,可能会导致程序出现错误或不正确的结果。在弹栈操作之前,应该先检查栈是否为空,以避免下溢。
所以顺序栈一定要注意判空和判满
两个栈共享一个空间
对于一个数组构成的栈,并不总是能刚好将数组填满,这样就会造成空间的浪费,那么如何提高空间的利用率,我们采用一种类似合租的方法,一个栈在前,一个栈在后,栈顶往中间移动,栈顶相遇即栈满,(这里只讲了两个栈共享,但实际上可以多个栈共享) ;
定义:
typedef struct {
int stack01[100] ;
int top01 ;
} Sqstack01;
typedef struct {
int stack02[100] ;
int top02 ;
} Sqstack02;
初始化:
Sqstack01* stack01 = (Sqstack01*) malloc(sizeof(Sqstack01)) ;
stack01->top01 = -1 ;//初始化
Sqstack02* stack02 = (Sqstack02*) malloc(sizeof(Sqstack02)) ;
stack02->top02 = 100 ;//初始化
压栈:
int push01 (Sqstack01* s, Sqstack02* q, int e) {
if (s->top01 + 1 == q->top02) {
printf("栈满,push失败") ;
return 0;
} else {
s->stack01[++s->top01] = e ;
return 1;
}
}
//push02类似
弹栈:
int pop01 (Sqstack01* s, Sqstack02* q, int *e) {
if (s->top01 == -1) {
printf("栈空, pop失败") ;
return 0;
} else {
*e = s->stack01[s->top01] ;
s->top01-- ;
return 1;
}
}
//pop02类似
链栈
链栈由单链表组成,元素通过结点压入栈中;
这里用一个写过的题目展示
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Node{
char data ;
struct Node* next ;
} StackNode;
typedef struct LNode {
StackNode* top ;
int count ;
} StackList;
void Push (StackList* L, char e) {
StackNode* s = (StackNode*) malloc(sizeof(StackNode)) ;
s->data = e ;
s->next = L->top ;
L->top = s ;
L->count++ ;
}
char Pop (StackList* L) { //没有判空栈
if (L->count == 0) {
return 0;
} else {
char e ;
e = L->top->data ;
L->top = L->top->next ;
L->count-- ;
return e;
}
}
int main(int argc, const char * argv[]) {
StackList* L = (StackList*) malloc(sizeof(StackList)) ;//创建链栈
L->count = 0 ;
L->top = NULL ;//初始化
char str[1000] ;
scanf("%s",str) ;
int len = (int)strlen(str) ;
for (int i =0 ; i < len / 2; i++) {
Push(L, str[i]) ;
}
for (int i = len / 2; i > 0; i--) {
if (L->top->data == str[len - i]) {
Pop(L) ;
}
}
if (L->top == NULL) {
printf("huiwen") ;
} else{
printf("no") ;
}
return 0;
}
链栈需要额外的指针空间来维护节点之间的连接关系,相对于顺序栈而言,占用更多的内存空间。
由于链栈使用链表实现,访问任意位置的元素需要遍历链表,因此随机访问性能较低,时间复杂度为O(n),其中n为链表的长度。
链栈的优点:
动态扩展:链栈的大小可以根据需要动态扩展,不受固定容量的限制。
灵活性:链栈可以处理任意大小的数据,不需要预先定义栈的最大容量。
内存管理:链栈使用动态内存分配,可以更灵活地管理内存,避免内存浪费。
队列
定义
队列(Queue)是一种常见的线性数据结构,遵循先进先出(First-In-First-Out,FIFO)的原则。队列中的元素在尾部插入,从头部移除。类比现实生活中排队等候的场景,最先进入队列的元素首先被处理或移出队列。
循环队列
对于顺序队列,我们需要在队头和队尾分别设置front和rear,以实现对头插入和队尾删除,但在空间有限的数组中,在删除时,为了减小时间复杂度,我们不会让所有元素前一,这也造成了有时队尾到头了,队头前却还有剩余空间,对于这种问题,我们采用循环队列的思想来解决;
循环队列中的队尾可以循环到数组的开头,形成一个环形结构。
每当队尾到头是,判断(rear+1)% size == front 来判断空间已满
(rear - front+size)%size获取长度 ;
(这里用的顺序队列)
定义:
typedef struct {
int data[20] ;
int front ;
int rear ;
} Sq;
计算队列长度:
int queueLength (Sq* s) {
return (s->rear - s->front +20) % 20;
}
入队:
int enqueue (Sq* s, int e) {
if ((s->rear + 1) % 20 == s->front) {
printf("队列已满") ;
return 0;
} else {
s->data[s->rear] = e ;
s->rear = (s->rear + 1) % 20 ;
return 1;
}
}
出队:
int dequeue (Sq* s, int* e) {
if (s->rear == s->front) {
printf("队列空") ;
return 0 ;
} else {
*e = s->data[s->front] ;
s->front = (s->front + 1) % 20 ;
return 1;
}
}
初始化:
Sq* s = (Sq*) malloc(sizeof(Sq)) ;
s->front = 0 ;
s->rear = 0 ;
循环队列的优点是充分利用了数组空间,避免了普通队列因出队操作导致的空间浪费。同时,循环队列的入队和出队操作时间复杂度都是O(1)。
循环队列是一种高效利用空间的队列实现方式,适用于需要频繁进行入队和出队操作的场景。
除了上面这种循环队列的方法,我们也可以设置flag来判断rear是在front前面还是后面,来判断队列满还是空 ;这种方法相对复杂一点,但与上面的方法相比,上面的方法会浪费一个元素的空间 ;
(链表实现的队列应该是差不多的,循环链表的话会更加简明) ;