C++数据结构(3.1):栈的链表实现详解
1 栈的基本概念与链表实现简介
栈是一种后进先出(LIFO, Last In First Out)的线性数据结构,只允许在栈顶进行插入和删除操作。栈的两个基本操作是压栈(push,将数据放入栈顶)和弹栈(pop,从栈顶移出数据)
。除了栈顶的元素外,其他元素都不可直接访问或修改。
栈可以通过数组或链表来实现
。链表实现栈的主要优势在于它可以动态地分配内存,不需要预定义大小,从而避免了数组实现中可能出现的栈满情况,提高了内存利用率。链表实现的栈只需要一个链接结点即可表示栈,然后再用一个整型变量表示栈的长度。
在计算机科学中,栈被广泛应用于编程语言的解析、内存管理、函数调用和算法实现等方面
。本文将详细讲解如何使用C++中的链表来实现栈数据结构。
2 链表节点的定义与栈类的结构
2.1 链表节点定义
在链表实现的栈中,每个节点需要包含两个部分:
- 数据域:存储栈中的元素值
- 指针域:指向下一个节点的指针
template<typename T>
struct Node
{
T data;
Node* next;
Node(T d) : data(d), next(nullptr) {};
};
上述代码定义了一个模板结构体Node,使用模板允许栈存储任意类型的数据。构造函数使用初始化列表来初始化节点的data和next指针,确保新创建的节点指向nullptr
。
2.2 栈类的结构
栈类需要维护两个核心成员变量:
- 头指针:指向栈顶节点的指针
- 大小计数器:记录当前栈中元素的数量
template<typename T>
class Stack
{
private:
struct Node
{
T data;
Node* next;
Node(T d) : data(d), next(nullptr) {};
};
int size;
Node* head;
public:
Stack() : size(0), head(nullptr) {};
~Stack();
void push(T element);
T pop();
T top() const;
int getsize() const;
};
栈类提供了基本的栈操作接口,包括构造函数、析构函数、压栈、弹栈、获取栈顶元素和获取栈大小等方法
。
3 核心功能实现:压栈(push)与弹栈(pop)
3.1 压栈操作
压栈操作即在栈顶添加一个新元素,对应于链表的头插法。新节点的next指针指向当前栈顶节点,然后头指针更新为新节点
template<typename T>
void Stack<T>::push(T element)
{
Node* newNode = new Node(element);
newNode->next = head;
head = newNode;
size++;
}
算法步骤:
- 创建一个新节点,存储要入栈的元素
- 将新节点的
next指针指向当前栈顶节点(head) - 更新
head指针,使其指向新节点 - 增加栈大小计数器
时间复杂度:O(1),因为只需要固定数量的操作
3.2 弹栈操作
弹栈操作即移除并返回栈顶元素。需要先检查栈是否为空,然后保存栈顶节点的数据,更新头指针指向下一个节点,并释放原栈顶节点的内存
template<typename T>
T Stack<T>::pop()
{
if (head == nullptr)
{
throw std::underflow_error("Stack is empty");
}
Node* temp = head;
T data = head->data;
head = head->next;
delete temp;
size--;
return data;
}
算法步骤:
- 检查栈是否为空,若空则抛出异常
- 保存当前栈顶节点的指针和数据
- 更新
head指针,使其指向下一个节点 - 释放原栈顶节点的内存
- 减少栈大小计数器
- 返回保存的栈顶数据
时间复杂度:O(1),因为只需要固定数量的操作
。
4 辅助功能实现:获取栈顶(top)与大小(getsize)
4.1 获取栈顶元素
获取栈顶元素操作返回栈顶节点的数据而不移除它。需要先检查栈是否为空,然后直接返回头指针所指节点的数据
template<typename T>
T Stack<T>::top() const
{
if (head == nullptr)
{
throw std::underflow_error("Stack is empty");
}
return head->data;
}
时间复杂度:O(1),因为只需要访问一个节点
4.2 获取栈大小
获取栈大小操作直接返回栈的大小计数器值
template<typename T>
int Stack<T>::getsize() const
{
return size;
}
时间复杂度:O(1),因为只需要返回一个整数值
5 内存管理:析构函数的实现
析构函数负责在栈对象销毁时释放所有节点的内存,防止内存泄漏。它通过循环删除所有节点来实现这一点
template<typename T>
Stack<T>::~Stack()
{
while (head)
{
Node* temp = head;
head = head->next;
delete temp;
}
}
算法步骤:
- 循环遍历栈中的所有节点
- 保存当前节点的指针
- 将头指针移动到下一个节点
- 删除保存的节点
- 直到头指针为
nullptr时停止
时间复杂度:O(n),其中n是栈的大小,因为需要遍历所有节点
。
6 代码测试与输出结果
为了验证栈的实现是否正确,我们可以编写测试代码来测试各种操作
#include <iostream>
#include <stdexcept>
using namespace std;
// 此处插入上述Stack类的实现
int main()
{
Stack<int> s;
// 测试压栈操作
s.push(1);
s.push(2);
s.push(3);
cout << "当前栈顶元素: " << s.top() << endl; // 应输出3
s.push(4);
cout << "当前栈顶元素: " << s.top() << endl; // 应输出4
// 测试弹栈操作
s.pop();
s.pop();
cout << "弹栈两次后栈顶元素: " << s.top() << endl; // 应输出2
cout << "当前栈大小: " << s.getsize() << endl; // 应输出2
return 0;
}
预期输出结果:
当前栈顶元素: 3
当前栈顶元素: 4
弹栈两次后栈顶元素: 2
当前栈大小: 2
7 链表实现的优势与适用场景
相比于数组实现,链表实现栈有以下优点
:
- 动态大小:链表的大小可以动态增减,不需要事先指定容量。可以根据实际情况灵活地分配内存,避免了数组实现中固定大小带来的限制。
- 内存利用率高:链表在需要增加或删除元素时,可以动态分配或释放内存,避免了固定容量带来的空间浪费。
- 高效的增删操作:在链表中插入或删除元素的时间复杂度为O(1),效率较高。
但链表实现也有一些缺点
:
- 需要额外的指针:链表节点除了存储元素本身的值,还需要一个指针来连接下一个节点,占用了额外的空间。
- 随机访问较慢:链表中的元素不是连续存储的,无法像数组一样通过索引进行快速随机访问。
适用场景:
- 当需要频繁进行插入和删除操作,并且栈的大小不确定时,链表实现的栈是更好的选择。
- 在内存分配方面,链表实现的栈更加灵活,因为它不需要连续的内存空间。
总结
本文详细介绍了如何使用C++中的链表来实现栈数据结构。链表实现的栈具有动态扩容、内存利用率高和操作高效等优点,特别适合元素数量变化大的场景
。通过模板编程,我们可以实现一个类型安全且通用的栈数据结构,能够存储任意类型的数据。
与数组实现的栈相比,链表实现的栈在插入和删除操作上更加高效,但在随机访问和内存开销方面略有不足
。在实际应用中,应根据具体需求选择合适的实现方式。
掌握栈的链表实现不仅有助于理解栈的基本原理,还能加深对链表操作和内存管理的理解,为进一步学习更复杂的数据结构打下坚实基础。
有用的话可以点点赞和关注!!!
源码附上:
#include<iostream>
#include<string>
#include<algorithm>
#include<stdexcept>
using namespace std;
template<typename T>
class Stack
{private:
struct Node
{
T data;
Node* next;
Node(T d) :data(d), next(NULL) {};
};
int size;
Node* head;
public:
Stack() :size(0), head(NULL) {};
~Stack();
void push(T element);
T pop();
T top()const;
int getsize()const;
};
template<typename T>
Stack<T>::~Stack()
{
while (head)
{
Node* temp = head;
head = head->next;
delete temp;
}
}
template<typename T>
void Stack<T>::push(T element)
{
Node* newNode = new Node(element);
newNode->next = head;
head = newNode;
size++;
}
template<typename T>
T Stack<T>::pop()
{
if (head == NULL)
{
throw std::underflow_error("Stack is empty");
}
Node* temp = head;
head = head->next;
delete temp;
size--;
}
template<typename T>
T Stack<T>::top()const
{
if (head == NULL)
{
throw std::underflow_error("Stack is empty");
}
return head->data;
}
template<typename T>
int Stack<T>::getsize()const
{
return size;
}
int main()
{
Stack<int> s;
s.push(1);
s.push(2);
s.push(3);
cout << s.top() << endl;
s.push(4);
cout << s.top() << endl;
s.pop();
s.pop();
cout << s.top() << endl;
cout << s.getsize() << endl;
return 0;
}

更多推荐

所有评论(0)