数据结构 —— 栈

本文讨论使用c语言实现栈。

什么是栈?

栈是一种后进先出的线性表。它只允许在表的一端进行插入和删除操作。在栈中的元素要满足先入栈的后出栈,而后出栈的元素先出栈。

什么意思?我们就把栈想象成一个杯子,现在要往杯子里面放物体;随着放入物体的增多,下面的物体要想拿出就必须等上面的物体拿出杯子后才能取出。

所以,栈是一种只能在一端进行插入和删除操作的线性数据结构。而能操作的这一端我们称为栈顶 (Top),另一端则为栈底(Bottom)。

在栈的实现中,常常有以下功能需要实现:

  • 初始化
  • 入栈(压栈):往栈中放入新元素。
  • 出栈:从栈中取出元素。
  • 读取栈顶元素
  • 判断栈的空与满

栈的实现

栈的实现有两种方式,为顺序栈和链式栈;就是用顺序表或链表来实现。我们依次来看。

一、使用顺序表实现栈

使用顺序表,即我们需要创建一个数组。为了更适合动态变化,我们需要对数组进行动态扩容;并且为了标记栈的栈顶,我们需要一个top始终指向栈顶,这是栈实现的核心。

来看代码:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int StackType;

typedef struct Stack
{
	StackType* data;
	int top;//栈顶指针,始终指向最顶端的元素
	int capacity;//数组容量,判断扩容
};

下面我们来实现功能。

1、初始化

初始化时,我们需要定义数组的初始大小,因此除了需要传入指针外,我们再传入一个初始的数组大小。

这里还需要注意的是,一般我们会将top赋值为-1,表示"始终指向栈顶元素的下标"。因此,初始化时栈没有元素,我们就先给一个-1。代码如下:

cpp 复制代码
void InitStack(Stack* p, int n)
{
	assert(p);

	p->data = (StackType*)malloc(sizeof(StackType) * n);
	if (p->data == NULL)
	{
		perror("malloc failed!");
		return;
	}
	p->capacity = n;
	p->top = -1;//栈顶,赋值-1表示为空栈
}

2、销毁

malloc出的内存都要记得销毁!

cpp 复制代码
void DestroyStack(Stack* p)
{
	assert(p);

	free(p->data);
	p->data = NULL;
	p->capacity = 0;
	p->top = -1;
}

3、入栈

直接看代码:

cpp 复制代码
void StackPush(Stack* p, StackType x)
{
	assert(p);

	if (p->top + 1 == p->capacity)
	{
		StackType* tmp = (StackType*)realloc(p->data, sizeof(StackType) * p->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc failed");
			return;
		}
		p->data = tmp;
		p->capacity *= 2;
	}
	p->data[++p->top] = x;
}

最后赋值的时候要先让top+1再赋值。

4、出栈

也是直接上代码:

cpp 复制代码
void StackPop(Stack* p)
{
	assert(p);

	if (p->top == -1)
	{
		printf("出栈失败!当前栈为空!\n");
		return;
	}
	p->top--;
}

这里直接使用top--来实现删除,实际上在内存中该元素还是存在,只是对栈来说不存在了,在下一次入栈时会将该部分覆盖掉。因此我们直接使用top--就能实现删除。

5、返回栈顶元素

这个函数就很简单了。因为p->top对应的就是栈顶元素的下标。

cpp 复制代码
StackType StackTop(Stack* p)
{
	assert(p);
	assert(p->top != -1);//保证非空
	return p->data[p->top];
}

6、测试

目前我们栈的功能已经实现完整了。下面我们写一个测试函数用于在main中使用:

cpp 复制代码
void Test()
{
    Stack s;
    InitStack(&s, 3); // 初始化容量为3

    printf("入栈\n");
    StackPush(&s, 10);
    StackPush(&s, 20);
    StackPush(&s, 30);

    // 测试自动扩容
    StackPush(&s, 40);
    printf("栈顶元素: %d\n", StackTop(&s)); // 应该是40

    printf("出栈\n");
    StackPop(&s);
    printf("栈顶元素: %d\n", StackTop(&s)); // 应该是30
    StackPop(&s);
    printf("栈顶元素: %d\n", StackTop(&s)); // 应该是20
    StackPush(&s, 50);
    printf("栈顶元素: %d\n", StackTop(&s)); // 应该是50

    // 出栈所有元素
    while (s.top != -1)
    {
        printf("出栈元素: %d\n", StackTop(&s));
        StackPop(&s);
    }

    DestroyStack(&s);
    printf("栈已销毁\n");
}

二、使用链表实现栈

链表栈会永远将栈顶放在头节点上便于O(1)操作。

1、头文件

结构体和我们写过的链表一样。

cpp 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int LSType;

typedef struct Node
{
	LSType data;
	struct Node* next;
}Node;

typedef struct LStack
{
	Node* _top;
};

void InitStack(LStack* p);
void DestroyStack(LStack* p);
void StackPush(LStack* p, LSType x);
void StackPop(LStack* p);
LSType StackTop(LStack* p);

2、源文件

明白了栈的原理,链表的实现也很简单。直接上代码:

cpp 复制代码
#include "ListStack.h"

void InitStack(LStack* p)
{
	assert(p);
 
	p->_top = NULL;
}

void StackPush(LStack* p, LSType x)
{
	assert(p);
	Node* newnode = BuyNewNode(x);
	newnode->next = p->_top;
	p->_top = newnode;
}

Node* BuyNewNode(LSType x)
{
	Node* newnode = (Node*)malloc(sizeof(Node));
	if (newnode == NULL)
	{
		perror("malloc falied!");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

void StackPop(LStack* p)
{
	assert(p);
	if (p->_top == NULL)
	{
		printf("该栈为空!\n");
		return;
	}
	Node* next = p->_top->next;
	free(p->_top);
	p->_top = next;
}

LSType StackTop(LStack* p)
{
	assert(p);
	assert(p->_top);
	return p->_top->data;
}

void DestroyStack(LStack* p)
{
	assert(p);

	Node* cur = p->_top;
	while (cur)
	{
		Node* tmp = cur;
		cur = cur->next;//必须在赋值之前
		free(tmp);
	}
	p->_top = NULL;
}

测试文件

最后给出测试文件:

cpp 复制代码
#include "Stack.h"
#include "ListStack.h"

void Test1()
{
    Stack s;
    InitStack(&s, 3); // 初始化容量为3

    printf("入栈\n");
    StackPush(&s, 10);
    StackPush(&s, 20);
    StackPush(&s, 30);

    // 测试自动扩容
    StackPush(&s, 40);
    printf("栈顶元素: %d\n", StackTop(&s)); // 应该是40

    printf("出栈\n");
    StackPop(&s);
    printf("栈顶元素: %d\n", StackTop(&s)); // 应该是30
    StackPop(&s);
    printf("栈顶元素: %d\n", StackTop(&s)); // 应该是20
    StackPush(&s, 50);
    printf("栈顶元素: %d\n", StackTop(&s)); // 应该是50

    // 出栈所有元素
    while (s.top != -1)
    {
        printf("出栈元素: %d\n", StackTop(&s));
        StackPop(&s);
    }

    DestroyStack(&s);
    printf("栈已销毁\n");
}

void Test2()
{
    LStack s;
    InitStack(&s);

    StackPush(&s, 10);
    StackPush(&s, 20);
    StackPush(&s, 30);

    printf("栈顶: %d\n", StackTop(&s)); // 30
    StackPop(&s);
    printf("栈顶: %d\n", StackTop(&s)); // 20

    StackPop(&s);
    StackPop(&s);

    StackPop(&s); // 栈为空提示

    DestroyStack(&s);
    printf("链表栈已销毁\n");
}

int main()
{
	Test1();
    Test2();
	return 0;
}

总结

通过上面的代码,我们已经掌握了使用顺序表和链表来实现栈,最后我们来对比总结一下两个方法。

顺序栈适用于元素数量已知或增长可控的场景,对访问速度要求高的场景。

链表栈使用于元素数量不确定,有频繁出入栈操作,且对内存使用灵活性要求高的场景。

相关推荐
编程之路,妙趣横生2 小时前
数据结构(十一) 哈希表
数据结构
客梦2 小时前
数据结构--队列
数据结构·笔记
xiaoxue..2 小时前
二叉搜索树 BST 三板斧:查、插、删的底层逻辑
javascript·数据结构·算法·面试
程序员小白条2 小时前
提前实习的好处有哪些?有坏处吗?
java·开发语言·数据结构·数据库·链表
蒙奇D索大2 小时前
【数据结构】排序算法精讲 | 快速排序全解:分治思想、核心步骤与示例演示
数据结构·笔记·学习·考研·算法·排序算法·改行学it
七夜zippoe2 小时前
Python高级数据结构深度解析:从collections模块到内存优化实战
开发语言·数据结构·python·collections·内存视图
sin_hielo11 小时前
leetcode 2483
数据结构·算法·leetcode
大头流矢12 小时前
归并排序与计数排序详解
数据结构·算法·排序算法
一路往蓝-Anbo12 小时前
【第20期】延时的艺术:HAL_Delay vs vTaskDelay
c语言·数据结构·stm32·单片机·嵌入式硬件