数据结构 - 栈

目录

一. 栈的概念

二. 栈的结构

三. 栈的实现

1. 实现栈的两种方式

链表实现栈

顺序表实现栈

选择依据

栈的创建

栈的初始化 

栈的销毁

入栈

出栈

获取栈顶元素

判断栈是否为空

获取栈中有效数据的个数


一. 栈的概念

栈(Stack)是一种重要的抽象数据类型(ADT),也是一种特殊的顺序表,用于存储有序的元素集合。它的最大特点是后进先出(Last In First Out, LIFO)。这意味着在这个数据结构中,最后添加的元素将是第一个被移除的。

栈的原型:其中进行数据插入的和删除操作的一端称为栈顶,另一端称为栈底。

栈的原则:栈中的数据元素遵守 后进先出(LIFO)的原则   

栈的两个典型操作:
压栈栈的插入操作叫做 进栈 / 压栈  / 入栈  (入数据在栈顶)

出栈:栈的删除操作叫做出栈。(出数据也在栈顶)

二. 栈的结构

三. 栈的实现

1. 实现栈的两种方式

针对栈的实现,我们可以使用之前学习过的链表顺序表都可以实现栈,但是我们需要考虑的什到底要用哪种方式实现栈呢?

链表实现栈

优点

  1. 动态大小:链表基于指针链接,因此可以根据需要动态增长和缩小,不需要事先定义容量。
  2. 内存利用:链表不需要连续的内存空间,因此在内存分配上比较灵活,适合元素数量不确定的情况。

缺点

  1. 内存开销:每个元素都需要额外的空间来存储指针,这增加了内存消耗。
  2. 性能问题:相较于顺序表,链表在内存中可能不连续,可能会影响缓存的利用效率,从而影响性能。

顺序表实现栈

优点:

  1. 快速访问:顺序表基于数组实现,可以实现快速的随机访问。
  2. 内存效率:不像链表,顺序表不需要额外的空间来存储指针,内存使用更加高效。
  3. 缓存友好:由于数据连续存储,顺序表更好地利用了CPU缓存机制,提高了访问效率。

缺点

  1. 固定大小:传统的数组大小是固定的,需要提前分配内存,虽然可以通过动态数组(如 C++ 的 std::vector 或 Java 的 ArrayList)来解决这个问题,但这可能导致复杂的扩容操作和潜在的性能损耗。
  2. 可能的扩容成本:当数组达到容量极限时,需要进行扩容,这涉及到分配新的更大的内存块,并复制旧数据到新位置,这个过程可能相对耗时。

选择依据

  • 性能需求:如果需要最大化性能,特别是访问速度,顺序表(数组)通常是更好的选择。对于频繁的插入和删除操作,链表可能更优。
  • 内存考量:如果内存使用更为关键,或者元素大小变化不可预测,链表提供了更灵活的内存管理。
  • 实现复杂性:数组的实现通常更简单直接。链表虽然提供更大的灵活性,但需要额外的指针操作和错误处理。

总的来说,没有绝对的“更好”,只有更适合特定需求的实现。例如,如果你预期栈会频繁变化大小,且不太关心随机访问性能,链表可能是一个好的选择。相反,如果你需要高性能和最优的空间利用,且栈的大小相对稳定,使用数组实现的顺序表可能更合适。

栈的创建

需要用结构体创建一个栈,这个结构体需要包括栈的基本内容(栈,栈顶,栈的容量)。

typedef int STDataType;
typedef struct Stack
{
    STDataType* a;       // 指向动态数组的指针,用于存储栈中的元素
    int top;             // 栈顶的索引,指向栈顶元素
    int capacity;        // 栈的容量,表示栈数组可以容纳的最大元素数量
} Stack;

栈的初始化 

主要是给 栈 一个地址空间,将栈中的数据数据全部清空,与便于后续的操作方便。

void StackInit(Stack* ps)
{
    assert(ps);
	ps->a = NULL;
    ps->capacity = 0;
    // 这里需要注意的是当 top=0 时指向的是栈顶元素的下一个位置
    //                   top=-1 时指向的是栈顶元素
    // 这里也可以理解为顺序表中 size 有效数据的意思
    ps->top = 0;
}

栈的销毁

栈的销毁,需要将动态数组的头指针 free 掉,结构体其他元素置0.

void StackDestroy(Stack* ps)
{
    assert(ps);  // 确保传入的栈指针不为空,防止未定义行为
    free(ps->a);  // 释放栈使用的动态数组内存
    ps->a = NULL;  // 将指向动态数组的指针设为NULL,避免悬挂指针(dangling pointer)
    ps->top = 0;  // 将栈顶索引重置为0
    ps->capacity = 0;  // 将栈的容量设置为0

    // 注意:此处没有释放栈结构本身的内存,仅释放了其内部的动态数组
}

入栈

需要判断 栈的容量是否已满,满了就取扩容,没满,则向栈顶插入数据即可

void STPush(Stack* ps, STDataType x)
{
	// 此时需要保证传入的栈,不是空
	assert(ps);

	// 扩容
	// 判断是否需要扩容
	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType* temp = (STDataType*)realloc(ps->a, sizeof(STDataType) * newcapacity);
		// 防止返回的是空指针
		if (temp == NULL)
		{
			perror("realloc fail!");
			exit(-1);
		}
		ps->a = temp;
		ps->capacity = newcapacity;
	}
	// 进行尾插数据
	ps->a[ps->top] = x;
	ps->top++;
}

出栈

只需要将栈顶的数据移除即可。

注意:一定要保证栈顶有数据

void StackPop(Stack* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));//检测栈是否为空

	ps->top--;//栈顶下移
}

获取栈顶元素

获取栈顶元素,即获取栈的最上方的元素。若栈为空,则不能获取。

STDataType StackTop(Stack* ps)
{
    assert(ps);                     // 确保传入的栈指针非空
    assert(!StackEmpty(ps));        // 确保栈不为空,防止访问空栈

    return ps->a[ps->top - 1];     // 返回栈顶元素
    //ps->a[ps->top - 1]:这部分代码访问栈中的数组 a,并获取栈顶元素。
    //这里使用 ps->top - 1 是因为 top 通常表示下一个可插入元素的位置,
    //所以栈顶的实际元素位于 top - 1 的位置。
}

判断栈是否为空

判断栈是否为空的函数通常检查栈顶指针top的值。如果top为0,则表示栈中没有元素,即栈为空

bool StackEmpty(Stack* ps)
{
    assert(ps);      // 断言检查,确保传入的栈指针非空
    return ps->top == 0; // 返回栈是否为空的判断结果
}

获取栈中有效数据的个数

因为top记录的是栈顶,使用top的值便代表栈中有效元素的个数。

int StackSize(Stack* ps)
{
	assert(ps);

	return ps->top;//top的值便是栈中有效元素的个数
}

  • 42
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值