学习笔记——单链表(C++描述)

在校菜鸡学生,可能表述上存在错误,如发现了恳请批评斧正。

单链表定义——什么是单链表

先前我们学习了顺序表的实现(链接:另一篇学习笔记——顺序表)。我们注意到顺序表也存在一些缺点:

  1. 为保证表的连续性,在插删适合需要移动过多的元素,当元素很多时候会导致性能下降得很明显。
  2. 在内存需求变化很大时候,可能会导致内存浪费或者不够用。

这时,我们想:加入我们使用一定空间,将数据之间的逻辑关系记录下来,是否可以解决上述问题。我们使用空间记录结点与结点的逻辑关系时,就有了单链表。
定义:单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表的基本元素是结点每个结点的构成 = 元素+ 指针。元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。

单链表的实现

结点的定义与实现

前面提到,一个结点有两个基本的属性:数据元素(data)指针(nextnode)数据元素用于存储数据,而指针用于指向当前结点的下一个结点的地方。
模拟单链表图

//单链表结点的定义
template <class T> class Node {
protected:
	T data; 	//数据
	Node<T>* nextnode; 	//指针指向下一结点
public:
	Node();//用于创建空的节点
	Node(const T& obj);//创建只知道数据的节点
	Node(const T& obj, Node<T>* address);//创建都知道的节点
	Node(const Node<T>& node);//复制节点


	virtual Node<T>* getNextNode();//返回下一节点地址
	virtual void setNextNode(Node<T>* const address);//设置下一节点
	virtual T showData();//返回数据

	~Node();//销毁节点
};
单链表的定义与实现

当我们具有结点(Node)的相关表述时,我们就可以对单链表进行表述了。与顺序表类似,对于单链表(LinkList)我们需要实现基本的增add()、插insert()、删remove()与查check()

template<class T> class LinkList {
protected:
	Node<T>* head;//指向链表表头
	int LENGTH;//链表的长度(不含头结点)

public:
	LinkList();

public:
	virtual void add(const T& obj); 	//往链表中添加数据
	virtual void insert(const T& obj, int index); 	//插入一个数据
	virtual T check(int index)const; 	//查看一个数据,需要提供下标
	virtual bool remove(int index); 	//删除一个数据
	virtual int length()const; 	//返回单链表的长度

public:
	virtual ~LinkList();
};
单链表的一个属性——链表头head

在上面的单链表表述过程中,我们发现单链表具有一个链表表头head。为什么需要设置这个头结点?为统一空表与非空表的操作而设置的
我们以add(const T& dt)为例子,查看是如何统一操作的:

  1. 当不设立链表表头。我们添加一个数据时候。需要检查head是否指向nullptr。若head指向nullptr,则直接添加数据;若没有指向nullptr,则需要从head开始,遍历链表直到查找到某一结点的下一结点指向nullptr(说明这是链表尾),添加数据。
  2. 若设立链表表头。我们只需以head为起点,向后遍历,知道某一结点的下一结点指向nullptr,说明当前结点时链表尾,添加数据。

从上方可以看到,设置链表表头head可以简化方法表述,起到统一操作的作用。

单链表模拟

创建一个空表

在这里我们创建一个空表,即完成LinkList的构造函数。

template<class T>
LinkList<T>::LinkList() {
	head = new Node<T>(); 	//创建一个链表头,即不含数据的Node对象
	LENGTH = 0; 	//空表长度为0
}
向单链表添加数据

实现一个数据结构,首先需要向这个结构添加数据。对于void add(const T&)方法,往下细分可以分为头插法与尾插法
头插法顾名思义是向单链表添加数据元素时,在链表头之后第一个位置添加元素。多次添加后,链表中离头结点最远的元素(即链表尾的元素)是最先进入的数据元素,最近的是最后添加的数据元素。
尾插法顾名思义是向单链表添加数据元素时,在链表尾添加元素。多次添加后,链表中离头结点最近的元素(即链表头的下一个结点的存储的数据元素)是最先进入的数据元素。
在这里我们给出尾插法得到方法实现:

template<class T>
void LinkList<T>::add(const T& obj) {
	Node<T>* temp = new Node<T>(obj); 	//创建新结点
	if(temp == nullptr) { return; } 	//数据无法存入,新结点创建失败,返回
	Node<T>* node = head; 	//游标(node)指向头结点(链表头)
	while(node->getNextNode() != nullptr) { 	//寻找结点尾
		node = node->getNextNode();  	//node指向下一结点
	}
	node->setNextNode(temp); 	//将结点连上链表 
	LENGTH++; 	//长度+1
}
向单链表插入数据元素

当向单链表插入某个数据时,我们需要知道,数据值obj与插入位置index,使用for循环查找到插入位置的前一位置是结点,完成插入。至于为什么寻找到插入位置的前一位置,因为
插入位置的前一结点(node)储存着插入节点的后一结点(node_A)的地址。插入时,需要将node的下一结点地址赋值给新结点node_new的属性nextnode,再将node的下一结点指向node_new.
如下图:
单链表插入算法

template<class T>
void LinkList<T>::insert(const T& obj, int index) {
	if (index <= 0 || index > SIZE) {	//下标错误,返回不做处理
		return;
	}
	Node<T>* temp = new Node<T>(obj); 	//创建新结点,含数据obj
	if(temp == nullptr) { return; } 	//创建结点失败,返回
	Node<T>* node = head; 	//游标指向链表表头head
	int i = 0;
	for (i = 0; i < index - 1; i++) { 	//寻找插入点的前驱
		node = node->getNextNode();
	}
	temp->setNextNode(node->getNextNode()); 	//设置新结点的后继
	node->setNextNode(temp); 	//设置插入点的前驱的后继
	LENGTH++; 	//长度+1
}
从单链表删除元素

当某一个数据元素我们不再需要时候,我们需要删除掉这数据。注意,由于我们均使用new创建对象,删除数据时需要释放(使用delete语句)掉相应的空间,防止内存泄漏。
删除与插入相似,即是insert()方法的逆过程。获取到删除点的前驱node_A后,存储需要删除的地址node_del,将node_del的后继传给node_A作为node_A的后继,删除node_del.
如下图:
单链表删除方法

template<class T>
bool LinkList<T>::remove(int index) {
	if (index <= 0 || index > SIZE) {
		return false;
	}
	int i = 0; 
	Node<T>* node = head; 
	for(i = 0; i < index - 1; i++) {
		node = node->getNextNode(); 
	}
	Node<T>* temp = node->getNextNode(); 
	node->setNextNode(node->getNextNode()->getNextNode()); 
	delete temp; 
	LENGTH--; 
	return true; 
 }

上面的删除方法是基于下标实现的,当然如果单链表需要实现多个相同数据的批量删除可以对remove()方法重载,提供如下接口

template<class T>
bool LinkList<T>::remove(const T& obj, bool (*comparator)(const T&, const T&)); 
/*
方法需要传入两个参数
obj和comparator
obj是删除样本,comparator为比较函数
当数据元素与样本在comparator返回为true时,说明是需要删除的元素。执行删除。
*/
查看链表中的一个元素

单链表查看即从链表头下一个结点开始,遍历查找。在这里提供基于下标查看的方法。

template<class T>
T LinkList<T>::check(int index)const {
	if (index <= 0 || index > SIZE) {
		return head->showData();
	}
	int i = 1; Node<T>* node = head->getNextNode();
	for (i = 1; i < index; i++) {
		node = node->getNextNode();
	}
	return node->showData();
}
析构函数

当程序运行结束时,需要回收单链表占用的空间,这时默认调用析构函数,完成对占用空间的释放。

template<class T>
LinkList<T>::~LinkList() {
	Node<T>* temp = head; 	//释放者,指向头结点,头结点也需要释放
	Node<T>* next = head->getNextNode(); 	//释放后是无法访问已释放的空间,需要提前记录下一结点位置
	while (temp != nullptr) {
		next = temp->getNextNode(); 	//获取下一个需要释放空间的地址
		delete temp; 	//释放当前结点空间
		temp = next; 	//将下一结点传递给释放者
	}
}
单链表的优劣
  1. 从上面的代码可以看到,相较于顺序表而言,单链表插删操作更加简单,无需移动其他操作。

加入需要删除或插入单链表中的Node结点的后一结点或插入一个结点,我们并不需要执行任何循环操作。时间复杂度为常数阶 O ( 1 ) O(1) O(1)而当指定删除某一下标的元素或插入元素至某一下标时,需要执行一个循环,这个循环受到链表长度影响,最大即为链表的长度,时间复杂度为 O ( n ) O(n) O(n)
2. 相较于顺序表,单链表的长度可以轻松应对空间需求不确定的情况。
3. 由于单链表中,需要额外的空间来存储逻辑结构nextnode,所以相较于顺序表,单链表的插删便捷是通过牺牲空间换取的。
4. 通过C++的指针与地址的相关知识。由于程序运行过程中,形如单链表的空间开辟方法,每个数据结点的存储地址是不确定的。所以若需要查找某一数据元素,是不能够像顺序表一样直接提取的,即便知道下标也如此,所以单链表带来方便的同时也导致了查找性能的下降。

关于头插法与尾插法

对于这两种方法的选择,个人认为有以下分析:

  1. 头插法是当一个LinkList对象执行add方法时,将数据插入到链表头(假设当前链表设置有链表头head)之后。这样得到的链表数据顺序与插入的数据先后相反。
  2. 尾插法是当一个LinkList对象执行add方法时,将数据插入到链表尾。这样得到的链表数据顺序与插入的数据先后一致。
  3. 与栈(一种后进先出的数据结构,LIFO)与队(一种先进先出的数据结构,FIFO)的相关知识相结合。
  4. 头插法更适合栈得到相关操作。最后进的数据元素必定在头结点之后,我们执行head->getnextNode()的方法读取头结点的下一位结点即可获取到存储地址,实现栈的相关操作。
  5. 尾插法更适合栈得到相关操作。最先进的数据元素必定在头结点之后,我们执行head->getnextNode()的方法读取头结点的下一位结点即可获取到存储地址,实现队的相关操作。
  6. 综上所述,当我们使用链表实现栈时,添加元素的方法适合使用头插法;而实现队时,添加元素方法更适合使用尾插法。
  7. 推广至广义层面上,当一个链表需要频繁对最后进的(部分)元素频繁操作时,适合使用头插法;需要对先进的(部分)元素频繁操作时,适合尾插法。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值