目录
栈
一,栈的基本概念
1,栈的定义
2,栈的常见操作
3,栈的插入与删除
4,进栈出栈变化形式
5,栈的顺序存储结构
6,两栈的共享空间
7.栈的链式存储结构
二,栈的应用——递归
队列
二,队列的基本操作
1,队列的定义
2,队列的常见基本操作:
3,循环队列
4,队列的链式存储结构
栈(stack)是限定仅在表尾进行插入和删除操作的线性表,栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)。
不含任何数据元素的栈称为空栈。
InitStack(*s):初始化操作,建立一个空栈S。
DestroyStack(*s):若栈存在,则销毁它。
ClearStack(*s):将栈清空。
StackEmpty(S):若栈为空,返回true,否则返回false。GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素。
Push(*S,e):若栈s存在,插入新元素e到栈S中并成为栈顶元素。
Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值。
StackLength(S):返回栈S的元素个数。
栈的插入操作,叫作进栈,也称压栈、入栈。
栈的删除操作,叫作出栈,也有的叫作弹栈。
最先进栈的元素,不一定最后出栈。栈对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制,也就是说,在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以。
举例来说,如果我们现在是有3个整型数字元素1、2、3依次进栈,会有哪些出栈次序呢?
第一种:1、2、3进,再3、2、1出。这是最简单的最好理解的一种,出栈次序为321。
第二种:1进,1出,2进,2出,3进,3出。也就是进一个就出一个,出栈次序为123。
第三种:1进,2进,2出,1出,3进,3出。出栈次序为213。
第四种:1进,1出,2进,3进,3出,2出。出栈次序为132。
第五种:1进,2进,2出,3进,3出,1出。出栈次序为231。
采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶元素的位置。
若存储栈的长度为StackSize=5,则栈顶位置top必须小于StackSize。当栈存在一个元素时,top等于0,因此通常把空栈的判断条件定位top等于-1。栈的普通情况,空栈,栈满如图所示。
顺序栈的进栈操作:
顺序栈的基本操作代码:
#include
#define MAXSIZE 50
typedef int E;
typedef struct Stack{ //栈的结构定义
E data[MAXSIZE];
E top;
}SqTack, *ArrayStack;
void InitStack(ArrayStack s) { //栈的初始化
s->top = -1;
}
void Push(ArrayStack s, E element) { //进栈
if (s->top == MAXSIZE - 1) {
return;
}
s->data[++s->top] = element;
}
void PrintStack(ArrayStack s) { //栈的打印
while(s->top != -1) {
printf("%d ", s->data[s->top--]);
}
}
void Pop(ArrayStack s, E* element) { //出栈
if (s->top == -1) {
return;
}
*element = s->data[s->top--];
}
int main(int argc, const char * argv[]) {
SqTack S;
InitStack(&S);
for (int i = 0; i < 50; i++) {
Push(&S, i);
}
int n;
Pop(&S, &n);
PrintStack(&S);
printf("%d ", n);
return 0;
}
共享栈的概念
利用栈底位置相对不变的特征,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸,如下图所示
栈满 :两个指针相差为1时,即top1 + 1 = top2。
共享栈的基本操作代码:
#include
#define MAXSIZE 50
typedef int E;
/*共享栈的空间结构*/
typedef struct Stack{
E data[MAXSIZE];
E top1;
E top2;
}SqStack, *ArrayStack;
void InitStack(ArrayStack s) {
s->top1 = -1;
s->top2 = MAXSIZE;
}
void Push(ArrayStack s, E element, E StackNumber) { //进栈
if (s->top1 + 1 == s->top2) { //栈满
return;
}
if (StackNumber == 1) {
s->data[++s->top1] = element;
} else if (StackNumber == -1) {
s->data[--s->top2] = element;
}
}
void PrintfStack(ArrayStack s) { //共享栈的打印
while (s->top1 != -1) {
printf("%d ", s->data[s->top1--]);
}
while (s->top2 != MAXSIZE) {
printf("%d ", s->data[s->top2++]);
}
}
void Pop(ArrayStack s, E* element, E StackNumber) { //出栈
if (StackNumber == 1) {
if (s->top1 == -1) {
return;
}
*element = s->data[s->top1--];
} else if (StackNumber == 2) {
if (s->top2 == MAXSIZE) {
return;
}
*element = s->data[s->top2++];
}
}
int main(int argc, const char * argv[]) {
SqStack S;
InitStack(&S);
int k = 1;
for (int i = 0; i < MAXSIZE; i++) {
Push(&S, i, k);
k = -k;
}
int n;
Pop(&S, &n, 1);
printf("%d \n", n);
Pop(&S, &n, 2);
printf("%d \n", n);
PrintfStack(&S);
return 0;
}
采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。这里规定链栈没有头节点,top为头指针。
链栈的进栈:
假设元素值为e的新节点是s,top为栈顶指针。
链栈的出栈:
假设变量p用来储存要删除的栈顶节点,将栈顶指针下移一位,最后释放p。
链栈的基本操作代码:
#include
#include
typedef int E;
/*链栈的定义结构*/
typedef struct StackNode {
E date;
struct StackNode* next;
}StackNode,*LinkStackPtr;
typedef struct Stack {
LinkStackPtr top;
E count;
} SqStack, *LinkStack;
void InitStack(LinkStack s) { //初始化
s->count = 0;
}
void Push(LinkStack S, E element) { //进栈
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->date = element;
s->next = S->top;
S->top = s;
S->count++;
}
void Pop(LinkStack S, E* element) { //出栈
LinkStackPtr p;
if (S->count == 0) {
return;
}
*element = S->top->date;
p = S->top;
S->top = S->top->next;
free(p);
S->count--;
}
void PrintfListStack(LinkStack S) { //打印
while (S->top) {
printf("%d ", S->top->date);
S->top = S->top->next;
}
}
int main(int argc, const char * argv[]) {
SqStack S;
InitStack(&S);
int len;
scanf("%d", &len);
for (int i = 0; i < 50; i++) {
Push(&S, i+1);
}
int n;
Pop(&S, &n);
printf("%d \n", n);
PrintfListStack(&S);
return 0;
}
若在一个函数、过程或数据结构的定义中又应用了它自身,则这个函数、过程或数据结构称为是递归定义的,简称递归。
例子 斐波那契额数列:
/*斐波那契数列的实现*/
int Fib(int n){
if(n == 0){
return 0; //边界条件
}else if(n == 1){
return 1; //边界条件
}else{
return Fib(n-1) + Fib(n-2); //递归表达式
}
}
那么我们讲了这么多递归的内容,和栈有什么关系呢?
这得从计算机系统的内部说起。前面我们已经看到递归是如何执行它的前行和退回阶段的。递归过程退回的顺序是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。
这种存储某些数据,并在后面又以存储的逆序恢复这些数据,以提供之后使用的需求,显然很符合栈这样的数据结构,因此,编译器使用栈实现递归就没什么好惊讶的了。
简单的说,就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。
当然,对于现在的高级语言,这样的递归问题是不需要用户来管理这个栈的,一切都由系统代劳了。
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。
允许插入的一端称为队尾,允许删除的一端称为队头。
initQueue(&Q):初始化队列,构造一个空队列Q。
QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
EnQueue(&Q, x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q, &x):出队,若队列Q非空,删除队头元素,并用x返回。
GetHead(Q, &x):读队头元素,若队列Q非空,则将队头元素赋值给x。
上图的rear 可以改为指向下标为0的位置,这样就不会造成指针指向不明的问题了,后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列
队列空:front == rear == 0;
队列满:
由于rear可能比front大,也可能比front小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相差整整一圈。所以若队列的最大尺寸为 QueueSize,那么队列满的条件是(rear+1) %QueueSize == front
队列长度:
当rear>front时,此时队列的长度为rear-front。但当rear
循环队列基础操作代码:
#include
#define MAXSIZE 10
typedef int E;
/*循环队列的基本结构*/
typedef struct Queue {
E data[MAXSIZE];
int front;
int rear;
}SqQueue, *ArrayStack;
void InitQueue(ArrayStack Q) { //初始化
Q->front = 0;
Q->rear = 0;
}
void EnQueue(ArrayStack Q, E element) { //入队
if ((Q->rear + 1) % MAXSIZE == Q->front) {
return;
}
Q->data[Q->rear] = element;
Q->rear = (Q->rear + 1) % MAXSIZE; //rear指针后移一位,若到最后则转到数组头部
}
void DeQueue(ArrayStack Q, E* element) { //出队
if (Q->front == Q->rear) {
return;
}
*element = Q->data[Q->front];
Q->front = (Q->front + 1) % MAXSIZE;
}
int QueueLength(ArrayStack Q) { //求队列长度
return (Q->rear - Q->front + MAXSIZE) % MAXSIZE;
}
void PrintfQueue(ArrayStack Q) { //打印队列
if (Q->front <= Q->rear) {
for (int i = Q->front; i < Q->rear; i++) {
printf("%d ", Q->data[i]);
}
} else {
for (int i = Q->front; i < MAXSIZE; i++) {
printf("%d ", Q->data[i]);
}
for (int i = 0; i < Q->rear; i++) {
printf("%d ", Q->data[i]);
}
}
}
int main(int argc, const char * argv[]) {
SqQueue q;
InitQueue(&q);
for (int i = 0; i < MAXSIZE - 1; i++) {
EnQueue(&q, i + 1);
}
int n;
DeQueue(&q, &n);
printf("%d\n", n);
EnQueue(&q, 10);
int length = QueueLength(&q);
printf("%d\n", length);
PrintfQueue(&q);
return 0;
}
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已, 我们把它简称为链队列。为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点。
空队列时
front和rear都指向头结点
链队列入队操作
入队操作时,其实就是在链表尾部插入结点
链队列的出队操作
出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,
若链表除头结点外只剩一个元素时,则需将rear指向头结点
链队列的基本操作代码
#include
#include
typedef int E;
/*链队列的基本结构*/
typedef struct QueueNode {
E data;
struct QueueNode* next;
}QueueNode, *LinkQueuePtr;
typedef struct Queue {
LinkQueuePtr front;
LinkQueuePtr rear;
}SqQueue, *LinkQueue;
void InitQueue(LinkQueue Q) {
LinkQueuePtr head = malloc(sizeof(QueueNode));
Q->front = head;
Q->rear = head;
}
void EnQueue(LinkQueue Q, E element) { //进队
LinkQueuePtr s = (LinkQueuePtr)malloc(sizeof(QueueNode));
s->data = element;
s->next = NULL;
Q->rear->next = s;
Q->rear = s;
}
void DeQueue(LinkQueue Q, E* element) { //出队
LinkQueuePtr p;
if (Q->front == Q->rear) {
return;
}
p = Q->front->next;
*element = p->data;
Q->front->next = Q->front->next->next;
if (Q->rear == p) {//要删除的节点是尾节点
Q->rear = Q->front;
}
free(p);
}
void PrintfQueue(LinkQueue Q) { //打印队列
LinkQueuePtr p = Q->front->next;
while (p) {
printf("%d ", p->data);
p = p->next;
}
}
int main(int argc, const char * argv[]) {
SqQueue q;
InitQueue(&q);
for (int i = 0; i < 50; i++) {
EnQueue(&q, i + 1);
}
int n;
DeQueue(&q, &n);
printf("%d ", n);
PrintfQueue(&q);
return 0;
}