俗话说得好,不懂链表的程序员,不配称为C/C++程序员。
为什么呢?
链表的存储主要依据指针来实现,而指针又是C/C++独有的特性,是其他语言没有的。
今天,你点进来看了这篇博客,说明你还是不懂C/C++当中链表的算法。
不懂没关系,看了这篇博客,只要是懂得指针的小伙伴,都会学会使用单向链表。
链表是线性表的链式存储方式,逻辑上相邻的数据在计算机内的存储位置不必须相邻,那么,怎么表示逻辑上的相邻关系呢?可以给每个元素附加一个指针域,指向下一个元素的存储位置。
如图:
从图中可以看出,每个结点包含两个域:数据域和指针域,指针域存储下一个结点的地址,因此指针指向的类型也是结点类型。
链表的核心要素:
链表分为三种:
今天这篇文章讲的是单向链表。
单向链表其结构体定义:
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
} LinkList, LinkNode;
单向链表的概念
如上图就是一条单向链表。链表的节点均单向指向下一个节点,形成一条单向访问的数据链。
链表我们把他分为头节点和节点,头节点默认是链表的头部,不存储数据,节点则存储所有数据。
如图就是一个典型的单向链表。头节点不存储数据,但是他的指针指向节点1的地址,所以头节点和节点1链接起来了。然后节点1的指针又指向了节点2的地址,使节点1和节点2也链接起来了…最后,节点i的指针指向节点n,最后一个节点,最后一个节点n的指针指向了NULL。至此,一条链条诞生了。
就好比如,头节点知道了节点1的地址,头节点就可以根据地址找到节点1;
就好比如,张三知道了李四家的地址,张三就可以根据地址找到李四。
因为他是单向链表,只能从头节点开始,一直到尾节点,不存在节点1的指针指向头节点,就好比如不存在李四能找到张三,只能张三找到李四。
讲到这里,可能还有很多朋友还是不懂链表,不过没关系,通过下面的例子,你就能完全掌握链表的用法。
// 定义链表
typedef struct Link {
int date; // 链表中的数据
struct Link* next; // 下一个节点地址
}LinkNode, LinkList; // LinkNode:节点 LinkList:头节点
// 初始化链表
bool initLink(LinkList* &L) {
// 参数一:单链表的头节点指针的引用
L = new LinkNode; // 分配内存
if (!L) {
// 是否分配失败
cout << "生成头节点失败!" << endl;
return false; // 生成节点失败
}
L->next = NULL; // 因为是初始化,所以头节点指向NULL
return true;
}
定义链表中,可以看到,他是一个结构体,date是该链表的数据,也可以是其他数据类型;struct Link* next;
该条语句是必须的,定义自己结构体的指针,用于存储下一个节点的地址,这也是上面说的指针。
LinkNode, LinkList;
,该两条语句是用于定义结构体变量的,两个用法都是一个的,只是取不同的名字,用于区分定义的结构体变量的含义。
我们再来看一下链表的初始化:
它就是要给函数接口,里面为结构体的变量分配内存,因为是初始化,里面没有存储数据,他的指针也就指向了NULL。
链表的初始化就是这么简单,因为我们定义的链表是指针类型的变量,所以获取里面的数据是需要使用”->
“来获取。
链表初始化后,链表仅只有一个头节点,没有其他节点(如上图)。
好了,链表初始化好后,我们就可以给他插入数据了。现在我们来学习一下头插法,就是在头部插入数据。
注意:他并不是在头节点的位置插入数据,而是在头节点的下一个节点,也就是节点1的位置插入数据。
头插法有两种情况:
情况一:链表中没有其他节点,只有头节点。
如图,它需要在头节点的后面插入节点。
情况二:链表中已有其他节点。
链表中有其他节点,头插法也一样还是在头节点的后面插入,插入后,新的节点就成为节点1,而原有的节点位置不变,节点名字加1.
虽然有两种情况,但是他们实现的代码都是一样的:
// 头插法
bool LinkInsert_front(LinkList*& List, LinkNode* Node) {
// 参数一:链表的头节点指针的引用;参数二:待插入的新节点
if (!List || !Node) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
Node->next = List->next; // 新插入节点的next指向头节点的下一个节点
List->next = Node; // 头节点的next指向新插入的节点
return true;
}
如上图就是他的连接过程。
我们需要将新插入的节点的指针next指向头节点的指针next原指向的节点,然后再将头节点的指针next指向新节点,就完成了插入。
顾名思义,尾插法就是在链表的最后面插入节点。
在链表尾部插入,必须得先找到尾节点。
他也有两种情况:
情况一:链表中没有其他节点,只有头节点。
因为只有头节点,所以头节点也就是最后一个节点,可以直接插入。
情况二:链表中已有其他节点。
我们需要找到尾节点,才可以插入。
虽然有两种情况,但是他们实现的代码都是一样的:
// 尾插法
bool LinkInsert_back(LinkList*& List, LinkNode* Node) {
// 参数一:链表的头节点指针的引用;参数二:待插入的新节点
if (!List || !Node) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkNode* p = List; // 定义临时节点指向头节点,用于找到尾节点
while (p->next) p = p->next; // 找到尾节点
Node->next = p->next; // 新插入节点的next指向NULL,因为尾节点的下一个节点必须是NULL值(也可以写成这样:Node->next = NULL;)
p->next = Node; // 旧尾节点的next指向新尾节点
return true;
}
我们需要定义临时节点,然后遍历指向最后一个节点的位置,就可以利用它插入新的节点了。
p = p->next
的意思是:假如p是代表节点1,那么p->next就是节点2,p = p->next
就是p要代表节点2了。
while(p->next)
,当p的下一个节点不为NULL的话,则行循环。当p->next
为NULL时,说明p已经在链表最后一个节点的位置了。
顾名思义,就是可以在链表的任何位置插入节点。
我们必须得找到插入位置节点的前一个节点,才可以进行插入。
// 任意位置插入
bool LinkInsert(LinkList*& List, int i, int& e) {
// 参数一:链表的头节点指针的引用;参数二:插入的位置;参数三:插入的元素
if (!List) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p, * s; // p用于寻找插入位置的前一个节点,s用于创建新节点待插入
p = List; // 将头节点赋值给p,用作下面循环查找
int j = 0; // 循环条件;因为从头节点开始,所以赋值0
while (p && j < i - 1) {
// 查找位置为i-1的节点,p指向该节点
p = p->next;
j++;
}
// 假如i大于链表的个数,则p为NULL,返回false;假如i为负数或者为零,则返回false
if (!p || j > i - 1) return false; // i值不合法的情况:i > n || i <= 0
s = new LinkNode; // 分配新节点内存
s->date = e; // 将元素赋值给新节点
s->next = p->next; // 新插入的节点指向插入位置后的下一个节点
p->next = s; // 插入位置的前一个节点指向新插入的节点
return true;
}
任意位置插入的算法难度在于如何找到插入位置的前一个节点。
代码中我们使用while循环进行查找,其中j < i-1,就是找到节点的关键条件。
到了这里,如果上面的链表代码都搞懂了的话,至此下面所讲的所有链表的用法都不难了。
获取任意位置节点的值和任意位置插入节点 代码实现都时差不多的,只是指定位置获取节点的值,只需要找到该节点就行了。而任意位置插入节点就是找到节点的上一个节点。
// 获取链表中指定节点位置的值
bool Link_GetElem(LinkList*& List, int i, int& e) {
// 参数一:链表的头节点指针的引用;参数二:查找的位置;参数三:获取它的值
if (!List || !List->next) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p = List->next; // 定义临时节点,用于寻找要获取的节点的值处
int j = 1; // 循环条件;因为是从第一个节点开始循环查找,所以赋值1
while (p && j < i) {
// 寻找到i的节点位置,p指向它
p = p->next;
j++;
}
// i>n,则p为NULL,所以返回false;i<=0,则j肯定是大于i的,所以返回false
if (!p || j > i) return false; // i值不合法的情况:i > n || i <= 0
e = p->date; // 将寻找到链表中的值赋值给引用变量e返回
return true;
}
此代码难点也是如何找到该节点位置。
就是通过一个值,去遍历整个链表,判断链表中是否有节点存储该值,有则返回该节点的位置。
// 查找该值在链表中节点的位置
bool LinkFindElem(LinkList*& List, int e, int& i) {
// 参数一:链表的头节点指针的引用;参数二:链表中查找的值;参数三:返回查找到的节点位置
if (!List || !List->next) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p = List->next; // 定义临时节点,用于寻找要查找的节点的值处
int j = 1; // 记录节点;因为是从第一个节点开始循环查找,所以赋值1
while (p && p->date != e) {
// 当前节点p不为NULL,而且当前节点的值不等于e时,执行循环,还未找到e
p = p->next;
j++;
}
if (!p) {
// 假如while循环没有找到,那么p为NULL,查无此值
i = 0; // 将i赋值零,查无此值
return false;
}
i = j; // 则行到这一步,说明找到了,将节点的位置赋值给i值返回
return true;
}
都是和上面差不多的代码,只是将位置条件判断换成了值的判断。
修改链表中节点的值。
代码就是和 ’获取链表中指定节点位置的值‘ 当中的代码一模一样,只是找到后,就将节点的值修改掉。
// 修改指定位置节点的值
bool LinkAlterValue(LinkList*& List, int i, int e) {
// 参数二:节点的位置;参数三:修改的值
if (!List || !List->next) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p = List->next; // 定义临时节点指向头节点的下一个节点
int j = 1; // 用于找到节点位置
while (p && j < i) {
// 找到节点位置,p指向它
p = p->next;
j++;
}
// i>n,触发条件一;i<=0,触发条件二
if (!p || j > i) return false; // i值不合法的情况:i > n || i <= 0
// 执行到这一步说明已经找到了需要修改值的节点,p指向它
p->date = e; // 将e值赋值给p的date
return true;
}
它也有两种情况:
情况一:根据节点的位置删除
// 删除链表中的一个节点:1.根据节点的位置删除
bool LinkDelete_1(LinkList*& List, int i) {
// 参数一:链表的头节点指针的引用;参数二:待删除节点的位置
if (!List || !List->next) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p, * q; // p用于寻找删除位置的前一个节点,q用于辅助删除和释放被删除节点的内存
p = List; // 将头节点赋值给p,用作下面循环查找
int j = 0; // 循环条件;因为从头节点开始,所以赋值0
while (p->next && j < i - 1) {
// 寻找到待删除节点的前一个节点位置,p指向它
p = p->next;
j++;
} // 循环结束时,p指向最后一个节点
// 当i>n || i<1,p的下一个节点为NULL,返回flase;当i<1,则为不合法,不可能删除头节点和负数位置的节点,返回false
if (!(p->next) || j > i - 1) return false; // 当i>n || i<1时,删除位置不合理
q = p->next; // 将待删除节点赋值给q
p->next = q->next; // 待删除节点的前一个节点指向待删除节点的后一个节点位置
delete q; // 释放掉待删除节点的内存
return true;
}
找到待删除节点的前一个节点,就可以删除了。
定义临时节点指向节点1,然后将节点1的next指向节点3,就完成了节点2的删除,最后定义临时节点指向节点2,将节点2的内存释放掉就行了。
情况二:根据节点的值删除
都一样的代码,找到待删除节点的前一个节点根据值判断寻找。就可以进行删除操作了。
// 删除链表中的一个节点:2.根据节点的值删除
bool LinkDelete_2(LinkList*& List, int e) {
// 参数一:链表的头节点指针的引用;参数二:待删除节点的值
if (!List || !List->next) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p, * q; // p用于寻找删除位置的前一个节点,p用于辅助删除和释放被删除节点的内存
p = List; // 将头节点赋值给p,用作下面循环查找
while (p->next && ((p->next)->date) != e) {
// 寻找到待删除节点的前一个节点位置,p指向它
p = p->next;
} // 循环结束时,p指向最后一个节点
if (!(p->next)) return false; // 如果p的下一个节点为NULL,则没有找到该值对应的节点
q = p->next; // 将待删除节点赋值给q
p->next = q->next; // 待删除节点的前一个节点指向待删除节点的后一个节点位置
delete q; // 释放掉待删除节点的内存
return true;
}
既然你使用了链表,那么就涉及到从堆栈非配内存的相关问题,当程序结束时,需要将链表的内存释放掉。
// 销毁单链表
void LinkDestroy(LinkList*& List) {
if (!List) {
// 合法性检查
cout << "链表为NULL!" << endl;
return;
}
cout << "链表销毁!" << endl;
LinkList* p = List; // 定义临时节点指向头节点,用于释放节点的内存
while (p) {
// 如果p为真,则继续则行循环,直到链表全部释放掉为止
cout << "删除节点:" << p->date << endl;
List = List->next; // 向下一个节点
delete p; // 释放掉当前节点的内存
p = List; // p移动到下一个节点
}
}
定义临时节点指向头节点,当临时节点不为NULL时,先将头节点指向下一个节点,然后释放掉临时节点指向的节点内存,再将头节点指向的下一个节点赋值给临时节点。如此循环释放,就可以将链表完整的释放掉了。
输出链表中的元素。
// 链表输出
void LinkPrint(LinkList*& List) {
// 链表
if (!List) {
// 合法性检查
cout << "链表为空!" << endl;
return;
}
LinkNode* p = List->next; // 定义临时链表指向头节点的下一个节点
while (p) {
// 如果不为空,执行
cout << p->date << "\t";
p = p->next; // p指向己的下一个节点
}
}
完整测试代码:
#include
#include
using namespace std;
// 定义链表
typedef struct Link {
int date; // 链表中的数据
struct Link* next; // 下一个节点地址
}LinkNode, LinkList; // LinkNode:节点 LinkList:头节点
// 初始化链表
bool initLink(LinkList* &L) {
// 参数一:单链表的头节点指针的引用
L = new LinkNode; // 分配内存
if (!L) {
// 是否分配失败
cout << "生成头节点失败!" << endl;
return false; // 生成节点失败
}
L->next = NULL; // 因为是初始化,所以头节点指向NULL
return true;
}
// 头插法
bool LinkInsert_front(LinkList*& List, LinkNode* Node) {
// 参数一:链表的头节点指针的引用;参数二:待插入的新节点
if (!List || !Node) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
Node->next = List->next; // 新插入节点的next指向头节点的下一个节点
List->next = Node; // 头节点的next指向新插入的节点
return true;
}
// 尾插法
bool LinkInsert_back(LinkList*& List, LinkNode* Node) {
// 参数一:链表的头节点指针的引用;参数二:待插入的新节点
if (!List || !Node) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkNode* p = List; // 定义临时节点指向头节点,用于找到尾节点
while (p->next) p = p->next; // 找到尾节点
Node->next = p->next; // 新插入节点的next指向NULL,因为尾节点的下一个节点必须是NULL值(也可以写成这样:Node->next = NULL;)
p->next = Node; // 旧尾节点的next指向新尾节点
return true;
}
// 任意位置插入
bool LinkInsert(LinkList*& List, int i, int& e) {
// 参数一:链表的头节点指针的引用;参数二:插入的位置;参数三:插入的元素
if (!List) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p, * s; // p用于寻找插入位置的前一个节点,s用于创建新节点待插入
p = List; // 将头节点赋值给p,用作下面循环查找
int j = 0; // 循环条件;因为从头节点开始,所以赋值0
while (p && j < i - 1) {
// 查找位置为i-1的节点,p指向该节点
p = p->next;
j++;
}
// 假如i大于链表的个数,则p为NULL,返回false;假如i为负数或者为零,则返回false
if (!p || j > i - 1) return false; // i值不合法的情况:i > n || i <= 0
s = new LinkNode; // 分配新节点内存
s->date = e; // 将元素赋值给新节点
s->next = p->next; // 新插入的节点指向插入位置后的下一个节点
p->next = s; // 插入位置的前一个节点指向新插入的节点
return true;
}
// 获取链表中指定节点位置的值
bool Link_GetElem(LinkList*& List, int i, int& e) {
// 参数一:链表的头节点指针的引用;参数二:查找的位置;参数三:获取它的值
if (!List || !List->next) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p = List->next; // 定义临时节点,用于寻找要获取的节点的值处
int j = 1; // 循环条件;因为是从第一个节点开始循环查找,所以赋值1
while (p && j < i) {
// 寻找到i的节点位置,p指向它
p = p->next;
j++;
}
// i>n,则p为NULL,所以返回false;i<=0,则j肯定是大于i的,所以返回false
if (!p || j > i) return false; // i值不合法的情况:i > n || i <= 0
e = p->date; // 将寻找到链表中的值赋值给引用变量e返回
return true;
}
// 查找该值在链表中节点的位置
bool LinkFindElem(LinkList*& List, int e, int& i) {
// 参数一:链表的头节点指针的引用;参数二:链表中查找的值;参数三:返回查找到的节点位置
if (!List || !List->next) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p = List->next; // 定义临时节点,用于寻找要查找的节点的值处
int j = 1; // 记录节点;因为是从第一个节点开始循环查找,所以赋值1
while (p && p->date != e) {
// 当前节点p不为NULL,而且当前节点的值不等于e时,执行循环,还未找到e
p = p->next;
j++;
}
if (!p) {
// 假如while循环没有找到,那么p为NULL,查无此值
i = 0; // 将i赋值零,查无此值
return false;
}
i = j; // 则行到这一步,说明找到了,将节点的位置赋值给i值返回
return true;
}
// 修改指定位置节点的值
bool LinkAlterValue(LinkList*& List, int i, int e) {
// 参数二:节点的位置;参数三:修改的值
if (!List || !List->next) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p = List->next; // 定义临时节点指向头节点的下一个节点
int j = 1; // 用于找到节点位置
while (p && j < i) {
// 找到节点位置,p指向它
p = p->next;
j++;
}
// i>n,触发条件一;i<=0,触发条件二
if (!p || j > i) return false; // i值不合法的情况:i > n || i <= 0
// 执行到这一步说明已经找到了需要修改值的节点,p指向它
p->date = e; // 将e值赋值给p的date
return true;
}
// 删除链表中的一个节点:1.根据节点的位置删除
bool LinkDelete_1(LinkList*& List, int i) {
// 参数一:链表的头节点指针的引用;参数二:待删除节点的位置
if (!List || !List->next) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p, * q; // p用于寻找删除位置的前一个节点,q用于辅助删除和释放被删除节点的内存
p = List; // 将头节点赋值给p,用作下面循环查找
int j = 0; // 循环条件;因为从头节点开始,所以赋值0
while (p->next && j < i - 1) {
// 寻找到待删除节点的前一个节点位置,p指向它
p = p->next;
j++;
} // 循环结束时,p指向最后一个节点
// 当i>n || i<1,p的下一个节点为NULL,返回flase;当i<1,则为不合法,不可能删除头节点和负数位置的节点,返回false
if (!(p->next) || j > i - 1) return false; // 当i>n || i<1时,删除位置不合理
q = p->next; // 将待删除节点赋值给q
p->next = q->next; // 待删除节点的前一个节点指向待删除节点的后一个节点位置
delete q; // 释放掉待删除节点的内存
return true;
}
// 删除链表中的一个节点:2.根据节点的值删除
bool LinkDelete_2(LinkList*& List, int e) {
// 参数一:链表的头节点指针的引用;参数二:待删除节点的值
if (!List || !List->next) {
// 合法性检查
cout << "链表为空!" << endl;
return false;
}
LinkList* p, * q; // p用于寻找删除位置的前一个节点,p用于辅助删除和释放被删除节点的内存
p = List; // 将头节点赋值给p,用作下面循环查找
while (p->next && ((p->next)->date) != e) {
// 寻找到待删除节点的前一个节点位置,p指向它
p = p->next;
} // 循环结束时,p指向最后一个节点
if (!(p->next)) return false; // 如果p的下一个节点为NULL,则没有找到该值对应的节点
q = p->next; // 将待删除节点赋值给q
p->next = q->next; // 待删除节点的前一个节点指向待删除节点的后一个节点位置
delete q; // 释放掉待删除节点的内存
return true;
}
// 销毁单链表
void LinkDestroy(LinkList*& List) {
if (!List) {
// 合法性检查
cout << "链表为NULL!" << endl;
return;
}
cout << "链表销毁!" << endl;
LinkList* p = List; // 定义临时节点指向头节点,用于释放节点的内存
while (p) {
// 如果p为真,则继续则行循环,直到链表全部释放掉为止
cout << "删除节点:" << p->date << endl;
List = List->next; // 向下一个节点
delete p; // 释放掉当前节点的内存
p = List; // p移动到下一个节点
}
}
// 链表输出
void LinkPrint(LinkList*& List) {
// 链表
if (!List) {
// 合法性检查
cout << "链表为空!" << endl;
return;
}
LinkNode* p = List->next; // 定义临时链表指向头节点的下一个节点
while (p) {
// 如果不为空,执行
cout << p->date << "\t";
p = p->next; // p指向己的下一个节点
}
}
int main(void) {
LinkList* list = NULL; // 链表头节点
LinkNode* node = NULL; // 新节点
// 初始化链表
if (initLink(list)) {
cout << "初始化成功!" << endl;
} else {
cout << "初始化失败!" << endl;
}
// 头插法
int n = 0;
cout << "请输入头插法需要插入的个数n:";
cin >> n;
while (n > 0) {
node = new LinkNode; // 分配一个节点内存
cin >> node->date;
LinkInsert_front(list, node); // 插入
n--;
}
// 尾插法
int nn = 0;
cout << "请输入尾插法需要插入的个数nn:";
cin >> nn;
while(nn > 0) {
node = new LinkNode; // 分配新节点内存
cin >> node->date;
LinkInsert_back(list, node);
nn--;
}
LinkPrint(list);
cout << endl;
// 任意位置插入
int nnn;
int i, date;
cout << "请输入任意位置需要插入的元素个数nnn:";
cin >> nnn;
while (nnn > 0) {
cout << "请输入插入位置和插入的元素:";
cin >> i >> date;
if (LinkInsert(list, i, date)) {
cout << "插入成功!" << endl;
} else {
cout << "插入失败!" << endl;
}
LinkPrint(list);
cout << endl;
nnn--;
}
// 获取链表中指定节点的值
int e = 0;
if (Link_GetElem(list, 3, e)) {
cout << "获取节点3成功,值为:" << e << endl;
} else {
cout << "获取节点3失败!" << endl;
}
// 查找链表中的值
if (LinkFindElem(list, 5, e)) {
cout << "查找值为5成功,节点位置为:" << e << endl;
} else {
cout << "查找值为5失败,节点位置返回:" << e << endl;
}
// 删除链表中的一个节点:1.根据节点的位置删除
if (LinkDelete_1(list, 3)) {
cout << "删除节点位置3成功!" << endl;
} else {
cout << "删除节点位置3失败!" << endl;
}
LinkPrint(list);
cout << endl;
// 删除链表中的一个节点:2.根据节点的值删除
if (LinkDelete_2(list, 1)) {
cout << "删除值为1的节点成功!" << endl;
} else {
cout << "删除值为1的节点失败!" << endl;
}
LinkPrint(list);
cout << endl;
// 修改指定位置节点的值
if (LinkAlterValue(list, 1, 44)) {
cout << "修改第一个节点的值成功!" << endl;
LinkPrint(list);
} else {
cout << "修改第一个节点的值失败!" << endl;
LinkPrint(list);
}
// 销毁单链表
LinkDestroy(list);
system("pause");
return 0;
}
总结:
链表其实不难,只需要认真学好我前面所讲的那几个操作,后面的那些都时融汇贯通的。