栈
栈是一种遵循后进先出(LIFO)的原则的数据结构。栈通常用于存储和管理函数调用、表达式求值、内存分配等操作中的临时数据。栈有两个基本操作:压栈(push),将数据放入栈顶;出栈(pop),从栈顶移出数据。除了栈顶的元素外,其他元素都不可直接访问或修改。栈可以通过数组或链表来实现。在计算机科学中,栈被广泛应用于编程语言的解析、内存管理和算法实现等方面。
数组实现
代码
/**
* 数组实现stack
*/
#include <cstdlib> // INT_MAX包含在cstdlib 头文件中
#include <cstring> // 使用cstring中的memcpy函数
#ifndef STACK_ARRAY_STACK_H
#define STACK_ARRAY_STACK_H
const int EXPAND_CAPACITY = 20; // 每次扩容大小
const int MAX_CAPACITY = INT_MAX; // 最大容量为INT_MAX
class Stack{
public:
void push(int e);
int top(){return data[size-1];} // 返回顶部数据
void pop(){data[size-1] = 0;size --;} // 弹出顶部数据
void clear(){
free(this->data);
data = (int *)malloc(sizeof(int)*10);
this->size = 0;
}
bool empty(){return size == 0;} // 栈是否为空
Stack(){
data = (int *)malloc(sizeof(int)*10); // data初始容量为10
}
~Stack(){free(this->data);}
private:
int *data; // 栈数据
int size = 0; // 元素个数
int capacity = 10; // data容量
};
/**
* 元素入栈
* @param e要入栈的元素
*/
void Stack::push(int e) {
// 如果元素个数>=栈容量,先扩容EXPAND_CAPACITY
if(size >= capacity){
capacity += EXPAND_CAPACITY;
// 如果扩容后的容量大于INT_MAX,直接扩容到INT_MAX
if(capacity >= MAX_CAPACITY){
capacity = MAX_CAPACITY;
}
int *t = (int *) malloc(sizeof (int) * capacity);
memcpy(t,data, sizeof(int) * size);
delete data;
data = t;
}
// size已达最大容量,不再执行push操作
if(size == MAX_CAPACITY) return;
data[size] = e;
size ++;
}
#endif //STACK_ARRAY_STACK_H
结构图
代码分析
上述代码实现了一个基于数组的栈数据结构。
其中,push()
方法实现了将元素入栈操作,如果当前栈已满,会自动对栈进行扩容。每次扩容的大小是 EXPAND_CAPACITY
,如果在扩容后的容量超过 INT_MAX
,则容量被限制为 INT_MAX
。
top()
方法用于返回栈顶元素。
pop()
方法则是弹出栈顶元素,并更新栈的大小。
clear()
方法用于清空栈元素。该方法首先释放原来的 data
数组,然后重新分配一个大小为10的数组,并将栈的大小更新为0。
该栈数据结构在构造函数和析构函数中分别使用了 malloc()
和 free()
方法来动态分配和释放内存,保证了随着程序的执行,栈可以正常添加和删除元素,同时也可以正确地释放占用的内存,避免了内存泄漏问题。
链表实现
代码
/**
* 链表实现stack
*/
#ifndef STACK_LINK_STACK_H
#define STACK_LINK_STACK_H
/**
* 链表节点定义
*/
class Node{
public:
int val; // data
Node *next; // 下一个节点
};
class LinkStack{
public:
void push(int e); // 从链表头部入栈
int top(){return this->first->next->val;} // 读取第一个元素
void pop(){Node *t = first->next;first->next = t->next;delete t;} // 弹出第一个元素
bool empty(){return this->first->next == nullptr;} // 是否为空
void clear(){
while(first->next != nullptr){
Node* head = first;
Node* temp = first->next;
delete head;
first = temp;
}
}
LinkStack(){first = new Node();} // 初始化first指针
~LinkStack(){this->clear();} // 释放内存
private:
Node *first; // 头节点,不存储数据
};
/**
* 入栈方法
* 新建val为e的节点node
* node->next 指向 first->next
* first->next 指向 node
* s++
* @param e 要入栈的元素
*/
void LinkStack::push(int e) {
Node *node = new Node();
node->next = nullptr;
node->val = e;
node->next = first->next;
first->next = node;
}
#endif //STACK_LINK_STACK_H
结构图
代码分析
上述代码实现了使用链表实现栈的数据结构。
首先定义了一个链表节点类 `Node`,包含一个整数值 `val` 和一个指向下一个节点的指针 `next`。
然后定义了一个链表栈类 `LinkStack`,其中包含了以下几个方法:
- `push(int e)`:从链表头部入栈,即在链表的首节点之后插入一个新的节点。新节点的值为 `e`,其指针指向原首节点,然后将首节点的指针指向新节点。栈的大小增加。
- `top()`:返回栈顶元素,即返回首节点的下一个节点。
- `pop()`:弹出栈顶元素,即将首节点的指针指向栈顶元素的下一个节点,然后释放原栈顶元素节点的内存。栈的大小减少。
- `empty()`:判断栈是否为空,即栈的大小是否为0。
类的构造函数 `LinkStack()` 初始化了首节点 `first`,构造函数创建了一个空的链表栈实例。
类的析构函数 `~LinkStack()` 释放了首节点 `first` 的内存。
不同实现的比较
数组实现栈
优点
- 简单且易于实现:数组在内存中是连续存储的,因此可以通过索引快速访问和操作元素。
- 随机访问:由于数组的连续存储方式,可以随机访问任意位置的元素,例如查询栈中的某个元素。
- 占用空间较小:数组中只需要存储元素本身,不需要额外的指针来连接节点。
缺点
- 固定大小:数组在创建时需要指定大小,无法动态调整。如果栈的大小超过了数组的容量,需要重新创建一个更大的数组并复制元素,效率较低。
- 空间浪费:如果栈的大小较小,而数组的容量较大,会导致部分空间被浪费。
链表实现栈
优点
- 动态大小:链表的大小可以动态增减,不需要事先指定容量。可以根据实际情况灵活地分配内存。
- 内存利用率高:链表在需要增加或删除元素时,可以动态分配或释放内存,避免了固定容量带来的空间浪费。
- 高效的增删操作:在链表中插入或删除元素的时间复杂度为O(1),效率较高。
缺点
- 需要额外的指针:链表节点除了存储元素本身的值,还需要一个指针来连接下一个节点,占用了额外的空间。
- 随机访问较慢:链表中的元素不是连续存储的,无法像数组一样通过索引进行快速随机访问。
综上所述,如果对栈的频繁插入和删除操作较多,并且栈的大小不确定,可以选择使用链表来实现栈。如果对随机访问有较高的需求,或者栈的大小是固定的,并且对空间利用率有较高要求,可以选择使用数组来实现栈。