单链表(ForwardList)
链表是链式存储结构的线性表的实现,由于内存地址不连续,所以链表不仅要有数据域,还要存储后继结点指针,如果一个结点只有一个指向后继结点的指针域,那么该线性表称为单链表
头指针与头结点
头指针中存储的是第一个结点的内存地址,通过头指针找到第一个结点,之后通过第一个结点的指针域可以找到第二个结点,以此类推,可以找到链表的最后一个结点,最后一个结点的指针域为空(nullptr)
为了更方便的对链表进行操作,引入了一个头结点的概念,头结点和普通结点一样,分为数据域和指针域,数据域可以不存东西也可以存如线性表的长度等附加信息,指针域存储的是第一个结点的内存地址
头指针和头结点的对比如下:
头指针
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则指向头结点
- 头指针是链表的必要元素
头结点
- 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可以存放链表的长度)
- 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了
- 头结点不一定是链表必须要素
如下实现使用头指针操作链表,遍历链表只能从头指针开始,所以访问链表中第一个结点的时间复杂度为O(1),访问最后一个结点的时间复杂度为O(N)
核心函数如下:
函数 | 功能 | 时间复杂度 |
---|---|---|
push_front | 向链表头部添加元素 | O(1) |
pop_front | 移除链表头部的元素 | O(1) |
push_back | 向链表尾部添加元素 | O(N) |
pop_back | 移除链表尾部的元素 | O(N) |
单链表操作尾部的元素需要从头部遍历到尾部之后才能操作,所以时间复杂度为O(N)
当然可以维护一个尾指针指向最后一个结点,但是这相当于空间换时间
#ifndef FORWARDLIST_H_
#define FORWARDLIST_H_
#include <iostream>
#include <tools/base/copyable.h>
#include <tools/base/noncopyable.h>
template <typename T>
class ForwardList : copyable {
private:
struct Node : noncopyable {
T data;
Node *next;
Node(T elem) : data(elem), next(nullptr) {}
};
public:
ForwardList() :
m_head(nullptr),
m_size(0){} // 默认构造函数
ForwardList(const ForwardList& other) : // 拷贝构造函数
m_head(nullptr),
m_size(other.size())
{
if(other.empty()){
return;
}
m_head = new Node(other.head()->data);
if(other.size() == 1){
return;
}
Node *preNode = m_head;
Node *selfNode;
Node *otherNode = other.head()->next;
while(otherNode != nullptr){
selfNode = new Node(otherNode->data);
preNode->next = selfNode;
preNode = preNode->next;
otherNode = otherNode->next;
}
}
~ForwardList(){
Node *curNode = m_head;
Node *nextNode;
while(curNode != nullptr){
nextNode = curNode->next;
delete curNode;
curNode = nextNode;
}
}
int size() const {
return m_size;
}
bool empty() const {
return m_size == 0;
}
Node *head() const {
return m_head;
}
void push_front(T elem){ // 向链表头部添加元素
Node *node = new Node(elem);
node->next = m_head;
m_head = node;
++ m_size;
}
void pop_front(){ // 移除链表头部的元素
if(m_head == nullptr){
return;
}
Node *node