什么是单链表
用链式存储方式实现的线性表称为链表。链表又可以分为:单链表、双链表、循环链表、静态链表。
顺序表与单链表的区别:
顺序表要求数据存储空间分布是连续的,单链表则不用,单链表利用指针将每个节点串起来,数据分布在内存中不要求连续。
节点:每个存储数据元素的内存空间被称为一个节点。
单链表中,每个节点都包含 数据域 和指针域。
单链表可以带头结点,也可以不带头结点,头结点的数据域一般为空,它的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)。