• 2022_08_05__106期__栈和队列


    目录

    用队列实现栈

    越界问题:

    栈的创建

    栈的插入:

    栈的首位

    栈的删除

    栈的判空:

    栈的销毁

    用栈实现队列:

    队列的定义:

    队列的创建:

    插入队列元素

    转化函数:

    队列元素的删除:

    找出栈顶的元素:

    判断队列是否为空

    队列销毁:

    设计循环队列:

    数组名:

    接下来,我们写数组的创建:

    数组判空

    数组判满

    入队列:

    出队列:

    取队头的数据:

    去队尾的数据:

    数组的释放:

    接下来,我们对代码整体分析:


    力扣

    用队列实现栈

    我们首先讲一下思路:

     我们要保持其中一个队列为空,另一个队列不为空。

    假如我们要删除数据的时候:

     对于队列来说,我们要删除队头的数据,也就是1,而对于栈来说,我们要删除的是队尾的数据,也就是4,所以我们要用两个队列来实现删除队尾的数据

    我们先取出三个元素到另一个队列

     然后我们可以直接取出元素4,然后删除掉

     接下来,我们继续删除数据,对于栈来说,我们要删除队尾的数据,也就是3,但对于队列,我们只能删除队头的元素,也就是1.

     然后我们取出3,删除掉。

     这里,我们就实现了栈的删除,接下来,我们思考如何实现栈的添加。

    假如我们要在这个时候,添加元素5和元素6

     我们只需判断,两个队列哪一个不为空,我们就添加元素5和6

     这就是整体的思路。

    我们首先写栈的创建,有些人是这样写的:

    1. MyStack* myStackCreate() {
    2. MyStack st;
    3. return &st;
    4. }

    这种写法是错误的,因为我们参数是在局部域中创建的,是局部变量,局部变量出函数自动销毁,我们返回参数的地址,但是我们创建的参数已经被释放,访问的话就形成了野指针。

    正确的写法:

    1. MyStack* myStackCreate() {
    2. MyStack*obj = (MyStack*)malloc(sizeof(MyStack));
    3. return obj;
    4. }

    我们会函数栈帧的形式进行解释:

     main函数调用栈的创建函数,然后我们函数返回,对应的栈帧销毁,返回对应的参数的地址。

     接下来,我们调用其他的函数:

     但是myStackPush1函数覆盖了原来的区域,所以对应的参数的值也发生改变,这时候的st的值已经不再是原来的值了。

    越界问题:

    越界读几乎不会产生问题:

    1. #include
    2. int main()
    3. {
    4. int a[10];
    5. printf("%d", a[11]);
    6. printf("%d", a[10]);
    7. printf("%d", a[12]);
    8. }

     这些越界访问都没有报错,但是假如我们进行越界写呢?

    1. #include
    2. int main()
    3. {
    4. int a[10];
    5. printf("%d", a[11]);
    6. printf("%d", a[10]);
    7. printf("%d", a[12]);
    8. a[10] = 1;
    9. a[11] = 2;
    10. a[12] = 3;
    11. }

    我们进行编译:

     这里报错了,那是不是所有的越界写都会报错呢?

    答案是不:

    1. int main()
    2. {
    3. int a[10];
    4. printf("%d", a[11]);
    5. printf("%d", a[10]);
    6. printf("%d", a[12]);
    7. a[12] = 3;
    8. }

    我们进行编译:

    发现这里并没有报错。

    总结:越界的检查其实是一种抽查。

    我们画图进行分析:

     越界是这样检查的,我们进行编译,编译器会检查粉色的区域是否被我们覆盖,假如没有覆盖时,我们不报错,假如覆盖的话,我们报错,所以假如我们越界访问的区域并不在粉色的区域,那我们进行越界写也不会报错。

    栈的创建

    1. MyStack* myStackCreate() {
    2. MyStack*obj=(MyStack*)malloc(sizeof(MyStack));
    3. QueueInit(&obj->q1);
    4. QueueInit(&obj->q2);
    5. return obj;
    6. }

    动态内存开辟一个栈的空间,然后对我们自己构建的栈中的两个队列进行初始化。

    栈的插入:

    1. void myStackPush(MyStack* obj, int x) {
    2. if(!QueueEmpty(&obj->q1))
    3. {
    4. QueuePush(&obj->q1,x);
    5. }
    6. else{
    7. QueuePush(&obj->q2,x);
    8. }
    9. }

    分析:如果队列1不为空时,我们向队列1插入元素x,否则向队列2插入元素。

    栈的首位

    1. int myStackTop(MyStack* obj) {
    2. if (!QueueEmpty(&obj->q1))
    3. {
    4. return QueueBack(&obj->q1);
    5. }
    6. else
    7. {
    8. return QueueBack(&obj->q2);
    9. }
    10. }

    分析:

     对于栈,我们要取出栈的首位,也就是4,所以对于队列,我们要取出队列队尾的数据。

    如果队列1不为空的话,我们返回队列1的末尾数据

    如果队列2不为空的话,我们返回队列2的末尾数据:

    栈的删除

    1. int myStackPop(MyStack* obj) {
    2. Queue*Empty = &obj->q1;
    3. Queue*nonEmpty = &obj->q2;
    4. if (!QueueEmpty(&obj->q1))
    5. {
    6. Empty = &obj->q2;
    7. nonEmpty = &obj->q1;
    8. }
    9. while (QueueSize(nonEmpty) > 1)
    10. {
    11. QueuePush(Empty, QueueFront(nonEmpty));
    12. QueuePop(nonEmpty);
    13. }
    14. int top = QueueFront(nonEmpty);
    15. QueuePop(nonEmpty);
    16. }

    我们画图来分析:

     我们要对栈进行删除,对栈删除时要首先删除4,所以我们要删除队列中的4.

    1. Queue*empty=&obj->q1;
    2. Queue*nonEmpty=&obj->q2;

    我们假设队列1为空,队列2不为空。

    1. if(!QueueEmpty(&obj->q1))
    2. {
    3. empty=&obj->q2;
    4. nonEmpty=&obj->q1;
    5. }

    假设队列1不为空的话,我们就设队列2为空,队列1不为空。

    1. while(QueueSize(nonEmpty)>1)
    2. {
    3. QueuePush(empty,QueueFront(nonEmpty));
    4. QueuePop(nonEmpty);
    5. }

    我们进行循环,当不为空的队列元素大于1时进入循环,我们把不为空的队列的头元素,插入到空队列,然后我们删除掉不为空队列,删除的是队头的元素。

    1. int top = QueueFront(nonEmpty);
    2. QueuePop(nonEmpty);

    我们已经删除到最后一个元素,QueueFront和QueueBack取出的结果都相同,我们把它赋给top,删除掉队列中的元素。

    栈的判空:

    1. bool myStackEmpty(MyStack* obj) {
    2. return QueueEmpty(&obj->q1)&&QueueEmpty(&obj->q2);
    3. }

    栈为空的条件是两个队列都必须为空。

    栈的销毁

    许多人会这样写:

    1. void myStackFree(MyStack* obj) {
    2. free(obj);
    3. }

    这样写是不对的,如图所示:

     假如我们把obj置为空指针,那么q1和q2指向的队列就没有释放,就造成了内存泄漏

    所以我们首先要把两个队列进行释放,再释放obj

    正确的写法是这样:

    1. void myStackFree(MyStack* obj) {
    2. QueueDestory(q1);
    3. QueueDestory(q2);
    4. free(obj);
    5. }

    下一道题目:

    力扣

    用栈实现队列:

    我们用图像进行分析:

     假设我们向栈1push四个元素1,2,3,4.结果为:

     对于队列来说,我们pop出的元素是1,而对于栈来说,我们pop出的元素为4,所以我们可以把栈1的元素导入到栈2中去:

     我们栈2pop出的元素是4:

     这时候,再pop的话,栈2pop的结果为3,和栈pop的结果是相同的。

    但是这里我们需要注意一点,就是这时候push元素的话,该怎么处理:

    我们可以push到栈1

    然后push完毕栈2的元素

     当栈2的元素为空时,把栈1的元素导入到队列2中去:

     然后我们pop出栈2的数据

     

     所以我们可以把栈1和栈2分别起另外一个名字:

     其中,st1代表我们push数据到st1,st2代表我们pop数据到st2.

    我们开始用代码编写:

    队列的定义:

    1. typedef struct {
    2. Stack pushST;
    3. Stack popST;
    4. } MyQueue;

    这里代表我们的队列是由两个栈组成的。

    队列的创建:

    1. MyQueue* myQueueCreate() {
    2. MyQueue*obj=(MyQueue*)malloc(sizeof(MyQueue));
    3. StackInit(&obj->pushST);
    4. StackInit(&obj->popST);
    5. return obj;
    6. }

    我们首先动态内存开辟一部分空间,供以结构体指针使用,然后通过结构体初始化两个栈,然后返回队列的指针。

    插入队列元素

    1. void myQueuePush(MyQueue* obj, int x) {
    2. StackPush(&obj->pushST,x);
    3. }

    非常简单,我们只需要往用于push的栈中push即可,与栈为空不为空没有任何关系。

    接下来,我们来分析,假如我们要写删除队列的某个元素:

    如图所示,逻辑是这样的,假如栈2不为空时,我们就逐步删除栈2的元素,假如栈2为空时,我们需要把栈1的全部元素push到栈2中去,然后删除栈1的元素,再逐步删除栈2的元素,所以我们的代码如图所示:

    所以我们首先要首先当栈为空时,对应的代码:

    我们需要创建一个函数

    转化函数:

    1. void pushSTToPopst(MyQueue*obj)
    2. {
    3. if(StackEmpty(&obj->popST))
    4. {
    5. while(!StackEmpty(&obj->pushST))
    6. {
    7. StackPush(&obj->popST,StackTop(&obj->pushST));
    8. StackPop(&obj->pushST);
    9. }
    10. }
    11. }

    接下来,我们来实现

    队列元素的删除:

    1. int myQueuePop(MyQueue* obj) {
    2. pushSTToPopst(obj);
    3. int front=StackTop(&obj->popST);
    4. StackPop(&obj->popSt);
    5. return front;
    6. }

    我们对代码进行分析:首先调用转化函数,调用完毕之后,栈2一定不为空,所以我们直接取出

    栈2的栈顶元素,栈顶元素就是栈要删除的元素,我们把它赋给front,然后调用删除栈元素函数删除掉这个栈顶元素,然后返回这个栈顶元素。

    找出栈顶的元素:

    1. int myQueuePeek(MyQueue* obj) {
    2. pushSTToPopst(obj);
    3. int front=StackTop(&obj->popST);
    4. return front;
    5. }

    找出栈顶的元素和删除队列的元素的对象都是栈2中的栈顶元素,无非就是找出栈顶的元素不需要删除,而删除队列的元素需要删除。

    判断队列是否为空

    1. bool myQueueEmpty(MyQueue* obj) {
    2. return StackEmpty(&obj->pushST)
    3. &&StackEmpty(&obj->popST);
    4. }

    队列为空的条件是两个栈都必须为空。

    队列销毁:

    1. void myQueueFree(MyQueue* obj) {
    2. StackDestory(&obj->pushST);
    3. StackDestory(&obj->popST);
    4. free(obj);
    5. }

    不能直接释放obj,原因是这样:

     我们的obj指向的是两个结构体指针,假如我们释放obj的话,是不能够把结构体指针指向的栈释放的,所以就会导致内存泄漏。

    力扣

    设计循环队列:

    我们首先画图分析一下思路:

     如图就是对应的环形缓冲器:

    因为我们的队列要保持先进先出:

     假如我们要删除元素,首先要删除front,假如我们删除两个元素:

     front也随着删除的移动而移动。

    假设我们要添加元素呢?

    假设我们添加两个元素:

     back也随添加元素移动而移动。

    虽然我们画图画的是环形的,但是我们实际上要写的代码要是线性的,我们提供两种不同的思路:

    首先,用数组的形式写:

     假设我们的数组位移动到6的后面时,我们直接指向1,形成这样的环

    第二种,链表的形式:

    最后的尾节点指向头,形成链表

    对于数组而言:假设我们要输入数据:

     

     假如我们要删除两个数据:

     

     假如我们这里再输入元素5

     对于链表,形式也是相同的。

    但是对于我们当前的链表和数组的问题是什么?

    不知道如何判断空和判断满

    例如:

    对于空数组:

     假设数组位空时:头指针和尾指针指向同一个位置:

    而对于满数组:

    我们进行逐渐输入元素来看数组内部指针的指向:

     

     

     

     可以发现,无论是空数组还是满数组,他们的首尾指针指向的位置都是相同的。

    对于链表也是相同的结果:

    假如我们删除两个元素呢?

    我们删除的是队头的元素:

     我们再插入两个元素:5 ,6

     back和front又指向了同一个位置。

    所以,无论是空数组还是满数组又或者是空链表还是满链表,头尾指针指向的位置都是相同的。

    这种问题有两种解决方法:

    1:加一个参数size,表示数组或者链表的元素个数,假如元素个数为0,就表示为空数组或者空链表,假如元素为创建的空间数:表示满数组和满链表。

    2:额外开辟一个空间,放在原空间的最后位置:

    原空间:

     新空间:

     我们进行输入元素

     当back的下一位指向首位,就说明数组已经满了,当back等于首位,就说明数组空了。

    我们用数组的写法写:

    数组名:

    1. typedef struct {
    2. int*a;
    3. int front;
    4. int back;
    5. int N;
    6. } MyCircularQueue;

    我们创建一个结构体,有四个成员变量,一起形成一个数组。

    分别表示数组名,数组头,数组位,和要创建的空间数:

    接下来,我们写数组的创建:

    1. MyCiMyCircularQueue* myCircularQueueCreate(int k) {
    2. MyCircularQueue*obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    3. obj->a=(int*)malloc(sizeof(int)*(k+1));
    4. obj->front=obj->back=0;
    5. obj->N=k+1;
    6. return obj;
    7. }

    k表示的数组元素个数,我们需要先开辟空间,这里必须开辟动态空间,假如开辟静态空间时,退出函数,对应的空间就销毁了。

    我们创建一个结构体指针obj,通过obj修改成员变量。

    我们要为数组开辟额外的空间,开辟的空间大小为k+1个整型空间,数组首元素的地址指向对应的空间。

    然后我们把成员变量中的头和尾初始化为0.把成员变量中要扩建的空间个数赋值为k+1.

    数组判空

    数组判空非常简单,就是我们之前分析的,假如数组头和数组尾重合时,数组就为空。

    1. bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    2. return obj->front==obj->back;
    3. }

     当数组头和数组尾重合时,就表示数组为空。

    数组判满

    数组判满也非常简单,当back的下一位为头,就表示数组满了

    1. bool myCircularQueueIsFull(MyCircularQueue* obj) {
    2. return (obj->back+1)==obj->front;
    3. }

    这样的写法对吗?是错误的:

    有特殊情况:

     这时候,back的下一位并不与front重合,所以我们可以使用余数的方法:

    1. bool myCircularQueueIsFull(MyCircularQueue* obj) {
    2. return (obj->back+1)%obj->N==obj->front;
    3. }

    入队列:

    1. bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    2. if(myCircularQueueIsFull(obj))
    3. {
    4. return false;
    5. }
    6. obj->a[obj->back]=value;
    7. obj->back++;
    8. obj->back%=obj->n;
    9. return true;
    10. }

    入队列时,我们要判断数组是否满了,假如数组满了的话,入队列失败。

    然后我们把输入的值valu赋给back指向的位置,然后back向后移,但是back也不能一直往后移动,当我们入队列到最后一位时,back要回到头位置。处理完毕,返回true。

    出队列:

    1. bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    2. if(myCircularQueueIsEmpty(obj))
    3. return false;
    4. obj-
    5. obj->front%=obj->n;
    6. return true;
    7. }

    假如数组为空时,不能删除元素,所以返回false

    数组不为空:在这里我们删除数组的元素,其实就是让front指针跳过这个元素即可,所以我们让front++,但是注意和上面相同的问题,当front到末位时,它的下一位是队头,所以我们要取余数,返回true。

    取队头的数据:

    1. int myCircularQueueFront(MyCircularQueue* obj) {
    2. if(myCircularQueueIsEmpty(obj))
    3. return -1;
    4. else
    5. return obj->a[obj->front];
    6. }

    如果数组中元素满了,我们返回-1.

    否则,我们返回front指向指向的数组元素。

    去队尾的数据:

    1. int myCircularQueueRear(MyCircularQueue* obj) {
    2. if(myCircularQueueIsEmpty(obj))
    3. return -1;
    4. else if(obj->back==0)
    5. return obj->a[obj->N-1];
    6. else
    7. return obj->a[obj->back-1];
    8. }

    我们的思路是返回back前一位的数组元素,但是还是有意外的情况:

     back在首位,back的前一位是末尾,我们如何取到末尾呢,所以我们就需要分情况讨论。

    对代码进行分析:假如数组为空,返回-1,退出函数

    假如数组不为空:假如我们的back在头,我们需要返回尾的元素,也就是下标obj->n-1对应的元素

    假如我们的back不在头,我们只需要返回obj->back-1对应的元素即可。

    接下来是

    数组的释放:

    我们释放空间需要分两部分,一部分是动态开辟赋给obj的空间,另一部分是动态开辟赋给obj->a的空间。

    1. void myCircularQueueFree(MyCircularQueue* obj) {
    2. free(obj->a);
    3. free(obj);
    4. }

    接下来,我们对代码整体分析:

    1. typedef struct {
    2. int*a;
    3. int front;
    4. int back;
    5. int N;
    6. } MyCircularQueue;
    7. MyCircularQueue* myCircularQueueCreate(int k) {
    8. MyCircularQueue*obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    9. obj->a=(int*)malloc(sizeof(int)*(k+1));
    10. obj->front=obj->back=0;
    11. obj->N=k+1;
    12. return obj;
    13. }
    14. bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    15. return obj->front==obj->back;
    16. }
    17. bool myCircularQueueIsFull(MyCircularQueue* obj) {
    18. return (obj->back+1)%obj->N==obj->front;
    19. }
    20. bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    21. if(myCircularQueueIsFull(obj))
    22. {
    23. return false;
    24. }
    25. obj->a[obj->back]=value;
    26. obj->back++;
    27. obj->back%=obj->N;
    28. return true;
    29. }
    30. bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    31. if(myCircularQueueIsEmpty(obj))
    32. return false;
    33. obj->front++;
    34. obj->front%=obj->N;
    35. return true;
    36. }
    37. int myCircularQueueFront(MyCircularQueue* obj) {
    38. if(myCircularQueueIsEmpty(obj))
    39. return -1;
    40. else
    41. return obj->a[obj->front];
    42. }
    43. int myCircularQueueRear(MyCircularQueue* obj) {
    44. if(myCircularQueueIsEmpty(obj))
    45. return -1;
    46. else if(obj->back==0)
    47. return obj->a[obj->N-1];
    48. else
    49. return obj->a[obj->back-1];
    50. }
    51. void myCircularQueueFree(MyCircularQueue* obj) {
    52. free(obj->a);
    53. free(obj);
    54. }

    1:数组的命名:

    1. typedef struct {
    2. int*a;
    3. int front;
    4. int back;
    5. int N;
    6. } MyCircularQueue;

    我们需要创建的是一个数组,a表示数组名,front表示数组头的下标,back表式数组尾的下一个的下标,N表示需要开辟多少空间。

    这里我们采用额外开辟一个空间的判空判满的方法:

    2:数组的创建:

    1. MyCircularQueue* myCircularQueueCreate(int k) {
    2. MyCircularQueue*obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    3. obj->a=(int*)malloc(sizeof(int)*(k+1));
    4. obj->front=obj->back=0;
    5. obj->N=k+1;
    6. return obj;
    7. }

    我们需要开辟两块空间,而且需要用动态内存开辟的方法,我们开辟一个空间给我们的结构体,然后我们再开辟一块空间给我们的数组,数组元素为k个时,我们需要开辟k+1个字节的空间

    然后我们顺表实现以下初始化,把数组头和数组尾赋为0,然后把N赋值为k+1.

    3:数组的判空:

    1. bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    2. return obj->front==obj->back;
    3. }

    我们使用bool类型,当front和back的下标相同,就表示数组为空。

    4:数组是否满的判定:

    1. bool myCircularQueueIsFull(MyCircularQueue* obj) {
    2. return (obj->back+1)%obj->N==obj->front;
    3. }

    数组满时基本的判定条件是back的下一位的坐标是front,我们可以采用取余数的方法实现。

    5:数组插入元素:

    1. bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    2. if(myCircularQueueIsFull(obj))
    3. {
    4. return false;
    5. }
    6. obj->a[obj->back]=value;
    7. obj->back++;
    8. obj->back%=obj->N;
    9. return true;
    10. }

    首先,我们要判定数组是否已经满了,假如数组满了的话,返回false。

    然后我们把value的值放在数组尾部的元素,然后数组尾往后移,取余数,执行完毕,返回true

    6:数组删除元素:

    1. bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    2. if(myCircularQueueIsEmpty(obj))
    3. return false;
    4. obj->front++;
    5. obj->front%=obj->N;
    6. return true;
    7. }

    如果链表为空时,无法删除,返回false

    我们删除元素只需要让front的下标往后移即可,然后再取余数,返回true。

    7:数组的头位置查找:

    1. int myCircularQueueFront(MyCircularQueue* obj) {
    2. if(myCircularQueueIsEmpty(obj))
    3. return -1;
    4. else
    5. return obj->a[obj->front];
    6. }

    我们首先进行判断:数组是否为空,如果为空的话,直接退出函数,不为空,返回front的下标对应的数组元素。

    8:数组尾元素的查找:

    1. int myCircularQueueRear(MyCircularQueue* obj) {
    2. if(myCircularQueueIsEmpty(obj))
    3. return -1;
    4. else if(obj->back==0)
    5. return obj->a[obj->N-1];
    6. else
    7. return obj->a[obj->back-1];
    8. }

    我们要先进性判断:数组是否为空,如果不为空的话,退出函数,然后当back在第一位元素时,我们返回数组最后一位元素。

    当back不在第一位元素时,返回back-1下标对应的元素。

    9:数组的释放:

    1. void myCircularQueueFree(MyCircularQueue* obj) {
    2. free(obj->a);
    3. free(obj);
    4. }

    我们需要先释放数组,然后再释放结构体,否则就会造成内存泄漏。

  • 相关阅读:
    java 实现单例模式
    web课程设计——健身俱乐部健身器材网站模板(24页)HTML+CSS+JavaScript
    软考之软件工程基础理论知识
    PHP毕业设计源代码计算机信息管理学院网站
    Python钢筋混凝土结构计算.pdf-已知弯矩确定混凝土梁截面尺寸
    类型体系与基本数据类型(第四节)
    Grafana+Prometheus+Pushgateway三剑客安装
    初始 JDBC
    Netty——网络编程 NIO(Selector处理write事件 写入内容过多问题)代码示例
    【Linux】进程间通信
  • 原文地址:https://blog.csdn.net/qq_66581313/article/details/126304175