线性表—单链表

什么是单链表

链式存储方式实现的线性表称为链表。链表又可以分为:单链表、双链表、循环链表、静态链表。

顺序表与单链表的区别:

顺序表要求数据存储空间分布是连续的,单链表则不用,单链表利用指针将每个节点串起来,数据分布在内存中不要求连续。

节点:每个存储数据元素的内存空间被称为一个节点。

单链表中,每个节点都包含 数据域 和指针域。


单链表可以带头结点,也可以不带头结点,头结点的数据域一般为空,它的next指向单链表中真正的第一个存储数据的节点(我们称为首节点)。

不带头节点的单链表和带头节点的单链表有什么不同:

  • 不带头节点的链表初始化时不创建任何节点。带头节点的链表初始化时要把头结点创建出来(可以把该头节点看成第0个节点) 。
  • 带头节点的单链表中的头结点不存放任何实际元素数据。头结点之后的下一个节点才开始存放数据。
  • 不带头结点的单链表在编写基本操作代码(插入,删除等)时更繁琐,往往需要对第一个或者最后一个数据节点进行单独处理。

c++模板类实现单链表

单链表的类定义、初始化操作

//单链表中每个节点的定义
template <typename T> //T代表数据元素的类型
struct Node
{
	T   data; //数据域,存放数据元素
	Node <T>* next; //指针域,指向下一个同类型(和本类型相同)节点。
};

//单链表的定义
template <typename T>
class LinkList
{
public:
	LinkList();  //构造函数
	~LinkList() {} //析构函数

public:
	bool ListInsert(int i, const T& e); //在第i个位置插入指定元素e

	bool InsertPriorNode(Node<T>* pcurr, const T& e);//在节点pcurr之前插入新节点

	bool ListDelete(int i); //删除第i个位置的元素

	bool GetElem(int i, T& e); //获得第i个位置的元素值
	int LocateElem(const T& e); //按元素值查找其在单链表中第一次出现的位置

	void DispList(); //输出单链表中的所有元素
	int ListLength(); //获取单链表的长度
	bool Empty();   //判断单链表是否为空
	void ReverseList(); //翻转单链表
private:
	Node<T>* m_head;  //头指针(指向链表第一个节点的指针,如果链表有头节点,则指向头结点)
	int m_length; //单链表当前长度(当前有几个元素),为编写程序更方便和提高程序运行效率而引入,但不是必须引入。
};

    
    //通过构造函数对单链表进行初始化
	template <typename T>
	LinkList<T>::LinkList()
	{
		m_head = new Node<T>; //先创建一个头节点
		m_head->next = nullptr;
		m_length = 0; //头节点不计入单链表的长度。

		//如果对于不带头的单链表:
		//m_head = nullptr;
		//m_length = 0;
	}

单链表常用操作

插入操作

    //在第i个位置(位置编号从1开始)插入指定元素e
	template <typename T>
	bool LinkList<T>::ListInsert(int i, const T& e)
	{
		//判断插入位置i是否合法,i的合法值应该是从1到m_length+1之间
		if (i < 1 || i >(m_length + 1))
		{
			cout << "元素" << e << "插入的位置" << i << "不合适,合法的位置是1到"
				<< m_length + 1 << "之间!" << endl;
			return false;
		}
		Node<T>* p_curr = m_head;

		//整个for循环用于找到第i-1个节点
		for (int j = 0; j < (i - 1); ++j) //j从0开始,表示p_curr刚开始指向的是第0个节点(头节点)
		{
			p_curr = p_curr->next; //pcurr会找到当前要插入的位置,比如要在第2个位置插入,pcurr会指向第1个位置
		}

		Node<T>* node = new Node<T>;  //(1)
		node->data = e;  //(2)
		node->next = p_curr->next; //(3) 让新节点链上后续链表,因为pcurr->next指向后续的链表节点。
		p_curr->next = node; //(4)让当前位置链上新节点,因为node指向新节点

		cout << "成功在位置为" << i << "处插入元素" << e << "!" << endl;
		m_length++;  //实际表长+1
		return true;
	}

	//在第i个位置(位置编号从1开始)插入指定元素e【不带头节点版本】
	/*
	template <typename T>
	bool LinkList<T>::ListInsert(int i, const T& e)
	{
		//判断插入位置i是否合法,i的合法值应该是从1到m_length+1之间
		if (i < 1 || i >(m_length + 1))
		{
			cout << "元素" << e << "插入的位置" << i << "不合适,合法的位置是1到"
				<< m_length + 1 << "之间!" << endl;
			return false;
		}

		if (i == 1) //插入到第一个位置与插入到其他位置不同,要单独处理
		{
			Node<T>* node = new Node<T>;
			node->data = e;
			node->next = m_head;
			m_head = node; //头指针指向新插入的第一个节点
			cout << "成功在位置为" << i << "处插入元素" << e << "!" << endl;
			m_length++;  //实际表长+1
			return true;
		}

		//插入的不是第一个位置则程序流程向下走
		Node<T>* p_curr = m_head;
		//整个for循环用于找到第i-1个节点
		for (int j = 1; j < (i - 1); ++j) //j从1开始,表示p_curr刚开始指向的是第1个节点
		{
			p_curr = p_curr->next; //pcurr会找到当前要插入的位置,比如要在第2个位置插入,pcurr会指向第1个位置(节点)
		}
		Node<T>* node = new Node<T>;
		node->data = e;
		node->next = p_curr->next; //让新节点链上后续链表,因为p_curr->next指向后序的链表节点
		p_curr->next = node; //当前位置链上新节点,因为node指向新节点
		cout << "成功在位置为" << i << "处插入元素" << e << "!" << endl;
		m_length++;  //实际表长+1
		return true;
	}
	*/

更好的插入操作实现方式:

  • 已知a2,要求往a2之前插入a5,简单方法是:
  •  往a2之后插入a5
  •  把a2和a5数据域交换
  • 这样时间复杂度是O(1),不用从前向后遍历寻找a2前一个节点了。
    template<typename T>
	bool LinkList<T>::InsertPriorNode(Node<T>* pcurr, const T& e)
	{
		//在节点pcurr之前插入新节点,新节点数据域元素值为e,请大家自行添加相关代码。。。。
		if(nullptr == pcurr)
			return false;
		if(pcurr == m_head->next)
		{
			//如果是在首节点之前插入节点
			Node<T>* new_node = new Node<T>;
			new_node->data = e;
			new_node->next = pcurr;//新节点的next指向之前的首节点
			m_head->next=new_node;//头节点的next指向新插入的节点
			return true;
		}
		//如果不是在首节点之前插入节点,那么我们就在pcurr节点之后插入节点,最后再交换两个节点的值。
		Node<T>* new_node = new Node<T>;
		new_node->data = e;
		new_node->next = pcurr->next;;//新插入节点的next指向pcurr的下一个节点
		pcurr->next = new_node;//pcurr的next指向新插入节点
		swap(pcurr->data,new_node->data);//交换两个节点的值,这样新节点就跑到原先pcurr前面去了
		return true;
	}

删除操作

    //删除第i个位置的元素
	template<typename T>
	bool LinkList<T>::ListDelete(int i)
	{
		if (m_length < 1)
		{
			cout << "当前单链表为空,不能删除任何数据!" << endl;
			return false;
		}
		if (i < 1 || i > m_length)
		{
			cout << "删除位置" << i << "不合法,合法的位置是1到" << m_length << "之间!" << endl;
			return false;
		}

		Node<T>* p_curr = m_head;//指向头结点
		//整个for循环用于找到第i-1个节点
		for (int j = 0; j < (i - 1); ++j) //j从0开始,表示p_curr刚开始指向额的是第0个节点(头节点)
		{
			p_curr = p_curr->next; //p_curr会找到当前要删除的位置所代表的节点的前一个节点的位置
		}
		Node<T>* p_willdel = p_curr->next; //p_willdel指向待删除的节点
		p_curr->next = p_willdel->next; //第i-1个节点的next指针指向了第i+1个节点
		cout << "成功删除位置为" << i << "的元素,该元素的值为" << p_willdel->data << "!" << endl;
		m_length--; //实际表长-1
		delete p_willdel;
		return true;
	}

    //删除pdel所指向的节点,请自行添加相关代码......
	template<typename T>
	bool LinkList<T>::DeleteNode(Node<T>* pdel)
	{
		/*
		将pdel下一个节点的值拷贝到pdel的数据域,将pdel的next指向下下个节点,释放pdel的下一个节点
		这样时间复杂度就是O(1),不用从前往后寻找pdel的前一个节点了。
		*/
		//特别注意书写和测试删除最后一个节点时可能遇到的问题....
		if(nullptr==pdel)
		{
			return false;
		}
		if(nullptr==pdel->next)
		{
			//如果是删除最后一个节点,只能从前往后找pdel的前一个节点
			Node<T>* pre_pdel = m_head;
			while(pre_pdel->next!=pdel)
			{
				pre_pdel=pre_pdel->next;
			}
			pre_pdel->next=nullptr;//删除节点的前一个节点指向空
			delete pdel;//释放最后一个节点的内存空间
			pdel=nullptr;//避免野指针
			return true;
		}
		Node<T>* next_pdel = pdel->next;
		pdel->data = next_pdel->data;//pdel的数据变成它下一个节点的数据,接下来要做的就是删除pdel的下一个节点
		pdel->next = next_pdel->next;//pdel指向它的下下个节点
		delete next_pdel;//释放之前pdel的下一个节点
		next_pdel=nullptr;//避免野指针
		return true;
	}

其它常用操作

    //获得第i个位置的元素值
	template<typename T>
	bool LinkList<T>::GetElem(int i, T& e) 
	{
		if (m_length < 1)
		{
			cout << "当前单链表为空,不能获取任何数据!" << endl;
			return false;
		}
		if (i < 1 || i > m_length)
		{
			cout << "获取元素的位置" << i << "不合法,合法的位置是1到" << m_length << "之间!" << endl;
			return false;
		}
		Node<T>* p_curr = m_head;
		for (int j = 0; j < i; ++j)
		{
			p_curr = p_curr->next;
		}
		e = p_curr->data;
		cout << "成功获取位置为" << i << "的元素,该元素的值为" << e << "!" << endl;
		return true;
	}

	
	//按元素值查找其在单链表中第一次出现的位置
	template<typename T>
	int LinkList<T>::LocateElem(const T& e)
	{
		Node<T>* p_curr = m_head;
		for (int i = 1; i <= m_length; ++i)
		{
			if(p_curr->next->data == e)
			{
				cout << "值为" << e << "的元素在单链表中第一次出现的位置为" << i << "!" << endl;
				return i;
			}
			p_curr = p_curr->next;
		}
		cout << "值为" << e << "的元素在单链表中没有找到!" << endl;
		return  -1; //返回-1表示查找失败
	}

	//输出单链表中的所有元素,时间复杂度O(n)
	template<typename T>
	void LinkList<T>::DispList()
	{
		Node<T>* p = m_head->next;
		while (p != nullptr) //这里采用while循环或者for循环书写都可以
		{
			cout << p->data << " "; //每个数据之间以空格分隔
			p = p->next;
		}
		cout << endl; //换行
	}

	//获取单链表的长度,O(1)
	template<typename T>
	int LinkList<T>::ListLength()
	{
		return m_length;
	}

	//判断单链表是否为空
	template<typename T>
	bool LinkList<T>::Empty()
	{
		if (m_head->next == nullptr) //单链表为空(如果是不带头节点的单链表if(m_head == nullptr)来判断是否为空。
		{
			return true;
		}
		return false;
	}

	//翻转单链表
	template<typename T>
	void LinkList<T>::ReverseList()
	{
		if (m_length <= 1)
		{
			//如果顺序表中没有元素或者只有一个元素,那么就不用做任何操作
			return;
		}
		/*
		头结点和首节点分为一组,剩余的其余节点为一组。先将首节点的next指向空,然后依次从剩余节点
		组成的组中拿出一个节点插到头结点之后。
		*/

		//至少有两个节点才会走到这里
		Node<T>* pothersjd = m_head->next->next; //指向从第二个节点开始的后续节点
		m_head->next->next = nullptr; //把第一个节点的指针域先置空。

		Node<T>* ptmp;
		while (pothersjd != nullptr)
		{
			//比如a1,a2,a3,a4共4个节点,第一次执行该循环时指向:注意下面代码注释
			ptmp = pothersjd;  //ptmp代表a2
			pothersjd = pothersjd->next; //pothersjd指向a3

			ptmp->next = m_head->next;  //a2指向a1
			m_head->next = ptmp;  //头节点指向a2
		}
	}

	//析构函数
	template<typename T>
	LinkList<T>::~LinkList()
	{		
		Node<T>* pnode = m_head->next;
		Node<T>* ptmp;
		while (pnode != nullptr) //该循环负责释放数据节点
		{
			ptmp = pnode;
			pnode = pnode->next;

			delete ptmp;
		}
		delete m_head; //释放头结点
		m_head = nullptr; //非必须
		m_length = 0; //非必须
	}

main函数测试

int main()
{
    LinkList<int> slinkobj;
    slinkobj.ListInsert(1, 12);
	slinkobj.ListInsert(1, 24);
	slinkobj.ListInsert(3, 48);
	slinkobj.ListInsert(2, 100);

    slinkobj.ListDelete(4);
	int eval = 0;
	slinkobj.GetElem(3, eval); //如果GetElem返回true,则eval中保存着获取到的元素值
	int findvalue = 100; //在单链表中要找的元素值
	slinkobj.LocateElem(findvalue);

    slinkobj.DispList();
	slinkobj.ReverseList();
	slinkobj.DispList();
    return 0;
}

总结

  • 单链表不需要大量连续存储空间存放数据元素,扩容方便;
  • 插入和删除节点方便。链表与数组比,更适合插入、删除 操作频繁的场景;
  • 存放后继指针要额外消耗内存空间。体现了利用空间换取时间来提高算法时间的编程思想;
  • 但对于内存紧张的硬件设备,就要考虑单链表是否合适;
  • 内存空间不连续所以无法实现随机访问链表中元素。沿着链逐个查找元素。O(n)。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心之所向便是光v

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值