数据结构与算法——栈和队列

本文详细介绍了数据结构中的栈和队列,包括它们的定义、存储结构(顺序栈、链栈、顺序队列、链队列)以及相关操作的实现。栈遵循后进先出原则,而队列则是先进先出。此外,还讨论了共享栈以节省空间,以及双端队列这一拓展概念。通过实例和代码展示了栈和队列的基本操作,如初始化、入栈、出栈、入队、出队等,帮助读者深入理解这些重要的数据结构。
摘要由CSDN通过智能技术生成

🚀前言

栈和队列是两种重要的线性结构。从数据结构角度看,栈和队列也是线性表,其特殊性在于栈和队列的基本操作是线性表操作的子集,它们是操作受限的线性表,因此,可称为限定性的数据结构。

在这里插入图片描述

但是从数据类型角度看,栈和队列是和线性表大不相同的两种重要的抽象数据类型。栈和队列的运用比较广泛,属于多型数据类型。



🚀栈(satck)


🚢栈的定义

(stack)是限定仅在表尾进行插入或删除操作的线性表。因此,对于栈来说,表尾端有其特殊的含义,称为栈顶(top),相应地,表头端称为栈底(bottom)。不包含元素的空表称为空栈

在这里插入图片描述

假设 S = ( a 1 , a 2 , . . . . a n ) S=(a1, a2,....an) S=(a1,a2,....an),则称 a 1 a1 a1为栈底元素, a n an an为栈顶元素。栈中元素按 a 1 , a 2 , . . . a n a1,a2,...an a1,a2,...an的次序进栈,那么出栈的第一个元素应为栈顶元素

栈的修改是按照后进先出的原则进行的,因此,栈又称为后进先出(last in first out)的线性表(简称LIFO)结构


🚢共享栈(节省空间)

两个栈共享一个存储空间,意义在于高效利用存储空间

📌第二种说法:两个栈底分别设置在一个空间的两端,栈顶向中间延伸


共享栈的定义

typedef struct Linknode{
    ElemType data;      //数据域
    struct Linknode *next;      //指针域
}LiStack;                   //栈类型定义

📌共享栈的栈满情况:当两个栈的top在空间中某一位置相遇时


🚢栈的表示和实现(顺序栈)

和线性表类似,栈也有两种存储表示方法——顺序栈和链栈

顺序栈,即栈的顺序存储结构是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附加指针top表示栈顶元素在顺序栈中的位置。通常当top=-1时,表示此栈为空栈。


👻顺序栈的定义
# define MaxSize 10         //定义栈中元素的最大个数
typedef struct{
    ElemType data[MaxSize];    //静态数组存放栈中元素 
    int top;                   //栈顶指针
}SqStack;

void testStack(){
    SqStack S;      //声明一个顺序栈(分配空间)
    .....
    //后续操作
}

由于栈在使用过程中所需要最大空间的大小很难估计,所以,一般来说,在初始化设空栈时不应限定栈的最大容量,常规做法是:先为栈分配一个基本容量,然后在应用过程中,当栈的空间不够使用时再逐步扩大容量


👻初始化操作

构造一个空栈,分配内存空间

# define MaxSize 10         //定义栈中元素的最大个数
typedef struct{
    ElemType data[MaxSize];    //静态数组存放栈中元素 
    int top;                   //栈顶指针
}SqStack;


void InitStack(SqStack &S){
    S.top = -1;             //初始化栈顶指针
}

void testStack(){
    SqStack S;      //声明一个顺序栈(分配空间)
    InitStack(S);
    .....
    //后续操作
}


//栈的判空操作
bool StackEmpty(SqStack S){
    if(S.top == -1){
        return true;        //栈空
    }
    else{
        return false;       //不为空
    }
}

第二种定义

在这里插入图片描述

按设定的初始分配量进行第一次存储分配,base可称为是栈底指针,在顺序占中,它始终指向栈底的位置,若base的值为NULL,则表明栈结构不存在,top表示栈顶指针,其初始值指向栈底,即top==base空栈可以表示为top==base。每当插入新的栈顶元素时,指针top增加1,删除栈顶元素时,指针top减1,因此,非空栈中的栈顶指针始终在栈顶元素的下一个位置上

#define STACK_INIT_SIZE 100;        //存储空间初始分配量
#define STACKINCREMENT 10;          //存储空间分配增量
typedef struct{
    SElemType *base;        //栈底指针
    SElemType *top;         //栈顶指针
    int stacksize;          //当前已分配的空间
}SqStack;

void InitStack(SqStack &S){
    //构造一个空栈
    S.base = (SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType));
    if(!S.base) exit(OVERFLOW);         //存储分配失败
    S.top = S.base;
    S.stacksize = STACK_INIT_SIZE;
}

void testStack(){
    SqStack S;          //声明一个顺序栈
    InitStack(S);
    ....
    //后续操作
}

👻进栈操作

若栈未满,则将x加入使之成为新栈顶

#define MaxSize 10;     //定义栈中元素的最大个数
typedef struct{
    ElemType data[MaxSize];     //静态数组存放栈中元素
    int top;                    //栈顶指针
}SqStack;


//新元素入栈
bool Push(SqStack &S, ElemType x){
    if(S.top == MaxSize-1){     //表示栈满了
        return false;
    }
    S.top = S.top + 1;          //指针先加一
    S.data[S.top] = x;          //新元素入栈
    return true;
}

👻出栈操作

若栈非空,则释放栈顶元素,并返回。

#define MaxSize 10;     //定义栈中元素的最大个数
typedef struct{
    ElemType data[MaxSize];     //静态数组存放栈中元素
    int top;                    //栈顶指针
}SqStack;


//出栈操作
bool Pop(SqStack &S, ElemType &x){
    if(S.top == -1){        //栈空,报错
        return false;
    }
    x = S.data[S.top];      //栈顶元素先出栈
    S.top = S.top - 1;      //指针再减1
    return true;
}

👻读取栈顶元素

若栈非空,则用x返回栈顶元素

#define MaxSize 10;     //定义栈中元素的最大个数
typedef struct{
    ElemType data[MaxSize];     //静态数组存放栈中元素
    int top;                    //栈顶指针
}SqStack;


//读取栈顶元素
bool GetTop(SqStack &S, ElemType &x){
    if(S.top == -1){        //栈空,报错
        return false;
    }
    x = S.data[S.top];      //记录栈顶元素
    return true;
}

🚢栈的表示和实现(链栈)

对于链栈的基本操作来说,和单链表的插入删除很类似,所以就不在赘述,链栈的入栈和出栈操作,其实就对应单链表的插入和删除操作


👻链栈的定义
typedef struct Linknode{
    ElemType data;      //数据域
    struct Linknode *next;      //指针域
}LiStack;                   //栈类型定义

栈的非法操作
📌上溢:当栈满了的情况下再次放入元素会造成此情况
📌下溢:当栈空了的情况下再次删除元素会造成此情况



🚀队列(queue)


🚢队列的定义


和栈相反,队列(queue)是一种先进先出(first in first out)的线性表缩写为FIFO)。它只允许在表的一端进行插入,在另一端进行删除元素。这种数据结构概括起来就和我们平时排队是一样的道理,最早进入到的队列的元素最先离开。

在这里插入图片描述


在队列中,只允许插入的一端叫做队尾(rear),允许删除的一端则称为队头(front)。假设队列为 q = ( a 1 , a 2 , . . . a n ) , q=(a1,a2,...an), q=(a1,a2,...an)那么, a 1 a1 a1就是队头元素, a n an an就是队尾元素。队列中的元素是按照 a 1 , a 2 , a 3.... a n a1,a2,a3....an a1,a2,a3....an的顺序进入的,退出队列也只能按照这个次序依次退出,也就是说,只有在 a 1 , a 2 , a 3... a n − 1 a1,a2,a3...an-1 a1,a2,a3...an1都离开队列之后, a n an an才能退出队列


  • 📌队首队尾指针的两种指法
    • ⭐队首指针(front)指向:队头元素的前一个存储位置

    • ⭐队尾指针(rear)指向:队尾元素

    • ⚡队首指针(front)指向:队头元素

    • ⚡队尾指针(rear)指向:队尾元素的下一个存储位置

📌假溢出:队中有空间,元素无法入队


🚢队列的顺序表示和实现(顺序队列)


和顺序栈类似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放队列头到队列尾的元素之外,还需要附设两个指针frontrear分别指示队列头元素以及队列尾元素的位置。基本操作基于循环队列,循环队列的引出是为了解决假溢出的问题。


  • 📌循环队列的性质
    • ⛅数组实现
      • 空队列:front == rear
      • 满队列:牺牲一个单元判满(不牺牲的话队空队满无法区分)
      • (rear+1)% maxSize == front
      • 进队:rear新 = (rear旧+1)% maxSize
      • 出队:front新 = (front旧+1)% maxSize
      • 队中元素个数/长度:(rear - front + maxSize) % maxSize

👻初始化操作

初始化队列,构造一个空队列

#define MaxSize 10         //定义队列中元素的最大个数
typedef struct{
    ElemType data[MaxSize];     //用静态数组存放队列元素
    int front, rear;            //队头指针和队尾指针
}SqQueue;

//初始化队列
void InitQueue(SqQueue &Q){
    //初始化 队头、队尾指针指向0
    Q.rear = Q.front = 0;
}

void testQueue(){
    //声明一个队列(顺序存储)
    SqQueue Q;
    InitQueue(Q);
    ...
    //后续操作
}


//判断队列是否为空
bool QueueEmpty(SqQueue Q){
    if(Q.rear == Q.front){  //队空条件
        return true;
    }else{
        return false;
    }
}

👻入队操作

若队列未满,将x加入,使之称为新的队尾

#define MaxSize 10         //定义队列中元素的最大个数
typedef struct{
    ElemType data[MaxSize];     //用静态数组存放队列元素
    int front, rear;            //队头指针和队尾指针
}SqQueue;

//入队
bool EnQueue(SqQueue &Q, ElemType x){
    if((Q.rear+1)%MaxSize == Q.front){       //判断队列是否已满
        return false;       //对满则报错
    }
    Q.data[Q.rear] = x;     //将x插入队尾
    Q.rear = (Q.rear + 1) % MaxSize;    //队尾指针后移
    return true;
}

👻出队操作

若队列非空,删除队头元素并返回x

#define MaxSize 10         //定义队列中元素的最大个数
typedef struct{
    ElemType data[MaxSize];     //用静态数组存放队列元素
    int front, rear;            //队头指针和队尾指针
}SqQueue;

//出队(删除一个队头元素,并返回x)
bool DeQueue(SqQueue &Q, ElemType &x){
    if(Q.rear == Q.front){      //判断队空
        return false;           //队空则报错
    }
    x = Q.data[Q.front];
    Q.front = (Q.front+1)%MaxSize;
    return true;
}

👻获取队头元素操作

读队头元素,若队列非空,则将队头元素赋值给x

#define MaxSize 10         //定义队列中元素的最大个数
typedef struct{
    ElemType data[MaxSize];     //用静态数组存放队列元素
    int front, rear;            //队头指针和队尾指针
}SqQueue;

//获得队头元素的值,用x返回
bool GetHead(SqQueue Q, ElemType &x){
    if(Q.rear == Q.front){
        return false;       //队空报错
    }
    x = Q.data[Q.front];
    return true;
}


🚢队列的链式表示和实现(链队列)


和线性表类似,队列也可以有两种存储表示

用链表表示的队列简称为链队列,一个链队列显然需要两个分别指示队头和队尾的指针(分别称为头指针和尾指针)才能唯一确定。

链队列的操作即为单链表的插入和删除操作的特殊情况,只是需要修改尾指针或头指针

一般情况下,删除队列头元素时仅需修改头结点中的指针,但当队列中最后一个元素被删除后,队列表尾指针也丢失了,因此需对队尾指针重新赋值(指向头结点)


👻初始化操作

初始化队列,构造一个空队列

📌带头结点:

typedef struct LinkNode{        //链式队列结点
    ElemType data;
    struct LinkNode *next;
}LinkNode;

typedef struct{     //链式队列
    LinkNode *front, *rear  //队列的队头和队尾指针
}LinkQueue;

//初始化队列(带头结点)
void InitQueue(LinkQueue &Q){
    //初始化 front、rear都指向头结点
    Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
    Q.front -> next = NULL;
}

viod testLinkQueue(){
    LinkQueue Q;        //声明一个队列
    InitQueue(Q);       //初始化队列
}

//判断队列是否为空
bool IsEmpty(LinkQueue Q){
    if(Q.front == Q.rear){
        return true;
    }else{
        return false;
    }
}

📌不带头结点:

typedef struct LinkNode{        //链式队列结点
    ElemType data;
    struct LinkNode *next;
}LinkNode;

typedef struct{     //链式队列
    LinkNode *front, *rear  //队列的队头和队尾指针
}LinkQueue;

//初始化队列(不带头结点)
void InitQueue(LinkQueue &Q){
    //初始化 front、rear都指向头结点
    Q.front = NULL;
    Q.rear = NULL;
}

viod testLinkQueue(){
    LinkQueue Q;        //声明一个队列
    InitQueue(Q);       //初始化队列
}

//判断队列是否为空
bool IsEmpty(LinkQueue Q){
    if(Q.front == NULL){
        return true;
    }else{
        return false;
    }
}

👻入队操作

若队列未满,将x加入,使之称为新的队尾
📌带头结点:

typedef struct LinkNode{        //链式队列结点
    ElemType data;
    struct LinkNode *next;
}LinkNode;

typedef struct{     //链式队列
    LinkNode *front, *rear  //队列的队头和队尾指针
}LinkQueue;

//新元素入队(带头结点)
void EnQueue(LinkQueue &Q, ElemType x){
    LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
    s->data = x;
    s->next = NULL;
    Q.rear->next = s;       //新结点插入到rear之后
    Q.rear = s;             //修改表尾指针
}

📌不带头结点:

typedef struct LinkNode{        //链式队列结点
    ElemType data;
    struct LinkNode *next;
}LinkNode;

typedef struct{     //链式队列
    LinkNode *front, *rear  //队列的队头和队尾指针
}LinkQueue;

//新元素入队(不带头结点)
void EnQueue(LinkQueue &Q, ElemType x){
    LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
    s->data = x;
    s->next = NULL;
    if(Q.front == NULL){        //在空队列中插入第一个元素
        Q.front = s;            //修改队头队尾指针
        Q.rear = s;
    }else{
        Q.rear->next = s;       //新结点插入到rear结点之后
        Q.rear = s;             //修改rear指针
    }
}

👻出队操作

若队列非空,删除队头元素并返回x

📌带头结点:

typedef struct LinkNode{        //链式队列结点
    ElemType data;
    struct LinkNode *next;
}LinkNode;

typedef struct{     //链式队列
    LinkNode *front, *rear  //队列的队头和队尾指针
}LinkQueue;

//队头元素出队操作(带头结点)
bool DEQueue(LinkQueue &Q, ElemType &x){
    if(Q.front == Q.rear){
        return      //空队列
    }
    LinkNode *p = Q.front->next;
    x = p->data;        //用变量x返回队头元素
    Q.front->nexy = p->nexy;        //修改头结点的next指针
    if(Q.rear == p){                //此次是最后一个结点出队
        Q.rear = Q.front            //修改rear指针
    }
    free(p);                        //释放结点空间
    return true;
}

📌不带头结点:

typedef struct LinkNode{        //链式队列结点
    ElemType data;
    struct LinkNode *next;
}LinkNode;

typedef struct{     //链式队列
    LinkNode *front, *rear  //队列的队头和队尾指针
}LinkQueue;

//队头元素出队操作(不带头结点)
bool DEQueue(LinkQueue &Q, ElemType &x){
    if(Q.front == Q.rear){
        return      //空队列
    }
    LinkNode *p = Q.front;      //p指向此次出队的结点
    x=p->data;                  //用变量x返回队头结点元素
    Q.front = p->next;          //修改front指针
    if(Q.rear == p){            //此次是最后一个结点出队
        Q.front = NULL;         //front指向NULL
        Q.rear = NULL;          //rear 指向NULL
    }
    free(p);                    //释放结点空间
    return true;
}

🚢双端队列

除了栈和队列之外,还有一种限定性数据结构——双端队列(deque)

双端队列是限定插入和删除操作在表的两端进行的线性表。着两端分别称作端点1端点2

在这里插入图片描述


在实际使用中,还可以有输出受限的双端队列(即一个端点允许插入和删除,另一个端点只允许插入的双端队列)和输入受限的双端队列(即一个端点允许插入和删除,另一个端点只允许删除的双端队列)。而如果限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为了两个栈底相邻接的栈了



💻总结

本节文章到这里就结束啦,以上内容涵盖数据结构与算法中栈和队列的基本概念以及基本操作,结合代码片段分析各基本操作的具体实现,在本文中,栈的链式存储以及循环队列是理解较为不易的点,需结合具体操作认真分析,希望各位小伙伴都能有所收获,一如既往希望我的文章能给各位小伙伴们带来帮助,数据结构与算法专栏也在持续更细中!!!

🎨觉得不错的话记得点赞收藏呀!!🎨

😀别忘了给我关注~~😀

骑士游历问题是一个经典的回溯问题,可以用栈和队列两种数据结构来解决。下面是Java代码以及详细注释: ```java import java.util.*; public class KnightTour { private int[][] board; // 棋盘 private int[] dx = {2, 1, -1, -2, -2, -1, 1, 2}; // 骑士移动的行坐标偏移量 private int[] dy = {1, 2, 2, 1, -1, -2, -2, -1}; // 骑士移动的列坐标偏移量 // 初始化棋盘 public KnightTour(int n) { board = new int[n][n]; } // 检查该位置是否可行 public boolean isSafe(int x, int y) { return (x >= 0 && x < board.length && y >= 0 && y < board.length && board[x][y] == 0); } // 使用栈来解决骑士游历问题 public boolean solveWithStack(int x, int y) { Stack<int[]> stack = new Stack<>(); // 存储骑士的位置 stack.push(new int[]{x, y}); board[x][y] = 1; // 标记该位置已经走过 while (!stack.empty()) { int[] curr = stack.peek(); // 获取当前骑士的位置 x = curr[0]; y = curr[1]; boolean found = false; // 尝试8个方向 for (int i = 0; i < 8; i++) { int nx = x + dx[i]; int ny = y + dy[i]; if (isSafe(nx, ny)) { stack.push(new int[]{nx, ny}); board[nx][ny] = board[x][y] + 1; // 标记该位置已经走过 found = true; break; } } // 如果无法走到下一个位置,则回溯 if (!found) { stack.pop(); board[x][y] = 0; // 取消该位置的标记 } // 如果已经访问了所有的位置,则找到了解 if (stack.size() == board.length * board.length) { return true; } } return false; } // 使用队列来解决骑士游历问题 public boolean solveWithQueue(int x, int y) { Queue<int[]> queue = new LinkedList<>(); // 存储骑士的位置 queue.offer(new int[]{x, y}); board[x][y] = 1; // 标记该位置已经走过 while (!queue.isEmpty()) { int[] curr = queue.poll(); // 获取当前骑士的位置 x = curr[0]; y = curr[1]; // 尝试8个方向 for (int i = 0; i < 8; i++) { int nx = x + dx[i]; int ny = y + dy[i]; if (isSafe(nx, ny)) { queue.offer(new int[]{nx, ny}); board[nx][ny] = board[x][y] + 1; // 标记该位置已经走过 } } // 如果已经访问了所有的位置,则找到了解 if (queue.size() == board.length * board.length) { return true; } } return false; } // 打印棋盘 public void printBoard() { for (int i = 0; i < board.length; i++) { for (int j = 0; j < board.length; j++) { System.out.print(board[i][j] + " "); } System.out.println(); } } public static void main(String[] args) { KnightTour kt = new KnightTour(8); int x = 0, y = 0; // 骑士的初始位置 kt.solveWithStack(x, y); kt.printBoard(); kt = new KnightTour(8); kt.solveWithQueue(x, y); kt.printBoard(); } } ``` 运行结果: ``` 1 60 39 34 31 18 9 64 38 35 32 61 10 63 30 17 59 2 37 40 33 28 19 8 36 49 42 27 62 11 16 29 43 58 3 50 41 24 7 20 48 51 26 45 22 15 64 12 57 44 53 4 23 6 21 25 52 47 56 55 46 13 14 5 1 28 39 34 31 18 9 64 38 35 30 61 10 63 20 17 29 2 37 40 33 28 19 8 36 49 42 27 62 21 16 19 43 30 3 50 41 24 7 22 48 51 26 45 22 15 64 11 57 44 53 4 23 6 23 25 52 47 56 55 46 13 14 5 ``` 注释: 1. `KnightTour`类表示骑士游历问题的解决方案。 2. `board`表示棋盘,`dx`和`dy`表示骑士在棋盘上移动的偏移量。 3. `isSafe`方法用于检查该位置是否可行,即该位置没有被访问过,并且在棋盘范围内。 4. `solveWithStack`方法使用栈来解决骑士游历问题。首先将骑士的初始位置入栈,然后不断尝试8个方向,直到找到解或无法继续移动。 5. `solveWithQueue`方法使用队列来解决骑士游历问题。首先将骑士的初始位置入队,然后不断尝试8个方向,直到找到解或无法继续移动。 6. `printBoard`方法用于打印棋盘。 7. 在`main`方法中,我们创建一个`KnightTour`对象,然后调用`solveWithStack`和`solveWithQueue`方法来解决骑士游历问题,并打印出棋盘。
评论 87
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小田是个程序员

各位看官,觉得不错就多支持哟!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值