先上代码,再进行解释
#include<iostream>
using namespace std;
/**
* @brief 创建一个结点的结构体,包含指针域和数据域
*/
typedef struct LinkNode {
int data;
struct LinkNode* next;
}LinkNode;
/**
* @brief 创建一个类,类中包含对链表的操作以及测试
*/
class LinkNode_Oper {
public:
/**
* @brief 初始化一个头结点
* @param 无
* @retval 返回头结点的地址
*/
LinkNode* LinkNode_Init()
{
LinkNode* Head = new LinkNode;
if (!Head)
{
cout << "申请内存失败!" << endl;
return NULL;
}
Head->data = 0;
Head->next = NULL;
return Head;
}
/**
* @brief 通过头插法在链表中插入结点
* @param 插入结点的数据
* @param 头结点地址
* @retval 返回是否添加成功,做调试用
*/
bool HeadInsert(int data, LinkNode* Head)
{
LinkNode* temp = new LinkNode;
if (!temp)
{
cout << "申请内存失败!" << endl;
return false;
}
temp->data = data;
temp->next = Head->next;
Head->next = temp;
return true;
}
/**
* @brief 通过尾插法在链表中插入结点
* @param 插入结点的数据
* @param 头结点地址
* @retval 返回是否添加成功,做调试用
*/
bool RearInsert(int data, LinkNode* Head)
{
LinkNode* temp = new LinkNode;
LinkNode* q = Head;
if (temp == NULL)
{
cout << "申请内存失败!" << endl;
return false;
}
temp->data = data;
temp->next = NULL;
while (q->next != NULL)
{
q = q->next;
}
q->next = temp;
return true;
}
/**
* @brief 通过指定位置在链表中插入结点
* @param 插入结点的数据
* @param 插入结点的位置
* @param 头结点地址
* @retval 返回是否添加成功,做调试用
*/
bool NodeList_Insert(int data, int pos, LinkNode* Head)
{
if (pos < 0)
{
cout << "位置不合法" << endl;
return false;
}
int curpos = 0;
LinkNode* temp, * q, * p;
q = Head;
p = Head;
temp = new LinkNode;
if (!temp)
{
cout << "申请内存失败!" << endl;
return false;
}
temp->data = data;
for (; curpos < pos; curpos++)
{
if (!p)
{
cout << "目标位置超过链表长度" << endl;
return false;
}
q = p;
p = p->next;
}
q->next = temp;
temp->next = p;
return true;
}
/**
* @brief 删除指定元素
* @param 指定结点的数据
* @param 头结点地址
* @retval 返回是否添加成功,做调试用
*/
bool NodeList_Delete(int data, LinkNode* Head)
{
LinkNode* q, * p;
q = Head;
p = Head;
while (q != NULL && q->data != data)
{
p = q;
q = q->next;
}
if (!q)
{
cout << "链表中未找到需要删除的元素:" << data << endl;
return false;
}
p->next = q->next;
delete q;
if (!q)
{
q = NULL;
}
return true;
}
/**
* @brief 显示链表数据域
* @param 头结点地址
* @retval 无
*/
void ShowList(LinkNode* Head)
{
LinkNode* q = Head;
while (q->next != NULL)
{
cout << q->next->data << " ";
q = q->next;
}
cout << endl;
}
/**
* @brief 测试函数
* @param 头结点地址
* @retval 无
*/
void test(LinkNode* Head)
{
cout << "头插法插入2、3、7并按如上顺序显示" << endl;
HeadInsert(7, Head);
HeadInsert(3, Head);
HeadInsert(2, Head);
ShowList(Head);
cout << "尾插法插入90、8、0并按如上顺序显示" << endl;
RearInsert(90, Head);
RearInsert(8, Head);
RearInsert(0, Head);
ShowList(Head);
cout << "在第三个位置插入-5" << endl;
NodeList_Insert(-5, 3, Head);
ShowList(Head);
cout << "删除元素2" << endl;
NodeList_Delete(2, Head);
ShowList(Head);
}
};
int main()
{
LinkNode_Oper operators;
LinkNode* Head = operators.LinkNode_Init();
operators.test(Head);
return 0;
}
测试结果:
以上代码主要涉及单链表的三个操作:初始化、插入和删除。
###初始化
我们先来讲讲初始化
一般而言,初始化一共分为三步
1.申请内存并做是否申请成功判断
2.初始化
3.返回
第一步很简单,掌握关键字new(c++)或者malloc(c)即可,至于判断是否申请成功则是为了以防我们之后出现操作野指针的情况
LinkNode* Head = new LinkNode;
if (!Head)
{
cout << "申请内存失败!" << endl;
return NULL;
}
在第二步中,由于我创建的链表是含有头结点的链表,所以第一个结点的数据域不包含有效数据,遂置0,而最重要的是指针域置NULL。因为不管是我们的尾插法还是遍历链表,都需要通过判断指针域是否为NULL来进行操作,如果我们在此时不置NULL,那么我们的操作就不能到达终止条件,会操作越界等问题,同时也会出现操作野指针的问题。
Head->data = 0;
Head->next = NULL;
第三步返回头结点的位置,这个不用多说。
###插入
接下来我们将插入操作,在图示代码中我运用了三种插入方法,分别是尾插法,头插法,和根据位置插入的方法,三种方法大同小异,我只讲头插入法。
插入也分为三步:
1.创建一个待插入结点并初始化
2.插入
3.返回
这里我们重点讲插入
头插法代码:
bool HeadInsert(int data, LinkNode* Head)
{
LinkNode* temp = new LinkNode;
if (!temp)
{
cout << "申请内存失败!" << endl;
return false;
}
temp->data = data;
temp->next = Head->next;
Head->next = temp;
return true;
}
这里我们借助图示可能讲得可能会更轻松
我们假设已经有链表中已经有三个结点,和一个待插入结点
我们知道,链表之所以能串联起来,是因为链表除了最后一个结点以外,每一个结点的指针域都存储着下一个结点的位置。所以我们可以通过前一个结点来访问下一个结点。而我们头插法所要达到的效果如图:
那么从插入前到插入后我们需要做几步呢?
两步
即让待插入结点的指针域指向头结点的下一个结点
头结点的指针域指向待插入结点
即
(这里temp就是待插入结点)
temp->next = Head->next;
Head->next = temp;
那可能有些人会疑惑,这里两行代码的顺序是否可以颠倒呢?
当然不能,这里也是插入操作的重点
这里我们假设结点地址为0xA0和0xB0,如果我们按照正常顺序来操作,在第一句代码temp->next = Head->next;
后,如图:
待插入结点的指针域指向了头结点的下一个结点,再来看第二句代码Head->next = temp;
后:
效果等价与该图:
而假如我们将两句代码颠倒顺序:
首先Head->next = temp;
,如图:
接下来:temp->next = Head->next;
这样操作的话,一个循环链表就做好了(bushi),并且我们会丢失后边的结点的地址,造成内存泄漏。
###删除
删除一个结点比较简单,包含以下几个步骤:
1.找到目标结点
2.删除目标结点
3.释放内存
代码:
bool NodeList_Delete(int data, LinkNode* Head)
{
LinkNode* q, * p;
q = Head;
p = Head;
while (q != NULL && q->data != data)
{
p = q;
q = q->next;
}
if (!q)
{
cout << "链表中未找到需要删除的元素:" << data << endl;
return false;
}
p->next = q->next;
delete q;
if (!q)
{
q = NULL;
}
return true;
}
这里我们主要讲解后面两个步骤,同样还是这张图:
我们删除第二个结点需要做什么?
将头结点的指针域跳过待删除结点即可:
即:p->next = q->next;
这样我们在访问的时候就可以跳过删除的结点了
另外:由于我们的结点存储在由我们自己开辟的动态内存,所以需要我们手动释放内存,否则会造成内存泄漏,所以需要delete q;
这一串代码,来释放内存。
以上就是单链表的知识总结。