目录
在设计单链表时,我们首先要引入单链表的定义,你首先得知道它是什么?
- 结点:用一组任意的存储单元存储线性表的数据元素(存储单元可以是连续的,也可以是不连续的),对其中一个数据元素来说,不仅要存储其本身的信息之外没还需存储一个指示其直接后继的信息(即直接后继的存储位置),这两部分信息组成这个数据元素的存储映像,称为结点。
- 线性链表/单链表:结点包括两个域,存储数据元素信息的被称为数据域,存储其直接后继存储位置的域称为指针域,指针域中存储的信息称作指针/链。多个结点链接成一个链表,即为线性表的链式存储结构,又由于此链表的每个结点中只包含一个指针域,故又称为线性链表或单链表。
- 基础设计:整个链表的存取必须从头指针开始进行头指针指示链表中第一结点(即第一个数据元素的存储映像)的存储位置,同时由于最后一个数据元素没有直接后继,则其指针为空。我们一般会在单链表的第一个结点之前附设一个结点,称之为头结点。头结点的数据域可以不存储任何信息,也可以存储如线性表的长度等类的附加信息,头结点的指针域存储直线法第一个结点的指针(即第一个元素结点的存储位置)
![](https://img-blog.csdnimg.cn/c081bcb9583343ecb796af3405b98c4b.png)
![](https://img-blog.csdnimg.cn/dfa1ee9e104c4bb4a2b5846eed3cf011.png)
而我们下来要设计的也就是带头结点的单链表。单链表头结点值保存第一个有效节点的地址,也就是只需要使用其指针域即可。
深入理解和设计:
结构体设计
我们先对其进行结构体设计,单链表里面的结点只有数据域和指针域,因此结构体里面的成员我们只需要设计两个就好了。
typedef struct Node {
ElemType data; //数据域
struct Node *next; //指针域
}Node,*PNode;
初始化
//初始化
void Init_List(PNode pn) {
assert(pn != NULL);
if (pn == NULL) return;
pn->next = NULL;
}
我们对其进行初始化的时候,也就是先对其头结点进行初始化,由于我们对其数据域不进行使用,因此是需要对其指针域进行初始化—— next -> NULL 就行了。
至于这里的 assert 和 if 两处的代码我在之前的不定长顺序表中有说过~大家可以过去看看,顺便对比一下顺序表和链表的区别。
附上链接~
数据结构——不定长顺序表_WLin.的博客-CSDN博客https://blog.csdn.net/m0_70184760/article/details/124641621?spm=1001.2014.3001.5501在下面的设计叙述中,大家一定要注意结点自身地址和此结点的指针域是不一样的,结点的指针域是其下一个要指向结点的自身地址,不要搞混淆了。
插入——头插
先申请一个新的结点,并对其进行初始化,然后让其指针域指向这个链表头结点的指针域,之后再让头结点的指针域指向我们申请的这个新结点的自身地址。
举个简单的例子叭:假设头结点为A,头结点后面的结点为B,我们申请的新结点为C。插入就是先让 C —> B,再让 A —> C
ps:不能先让头结点指向新结点的自身地址,即也就是 A —> C,再让 C —> B,这样是一定不可以的!再A—>C的时候,就会断开A和B的联系,造成B后面的数据缺失,就找不到B了,C也不可能指向B
//头插
bool Insert_head(PNode pn, ElemType val) {
assert(pn != NULL);
if (pn == NULL) return false;
struct Node* pnewnode = (struct Node*)malloc(1 * sizeof(struct Node));
assert(pnewnode != NULL);
if (pnewnode == NULL) return false;
pnewnode->data = val;
pnewnode->next = NULL;
pnewnode->next = pn->next; //让新结点的指针域指向头结点指向其后继结点的地址
pn->next = pnewnode; //让头结点指向新结点的地址
return true;
}
插入—— 尾插
那既然是尾插,那我们肯定要先找到链表的尾部,这个时候就需要定义一个指针来替我们找到链表的尾部,并且链表的尾部结点的指针指向NULL,那么我们就需要修改尾结点的指针域,让其指向我们申请新结点的地址,并让新结点的指针域指向NULL。
//尾插
bool Insert_tail(PNode pn, ElemType val) {
assert(pn != NULL);
if (pn == NULL) return false;
struct Node* pnewnode = (struct Node*)malloc(1 * sizeof(struct Node));
assert(pnewnode != NULL);
if (pnewnode == NULL) return false;
pnewnode->data = val;
pnewnode->next = NULL;
struct Node* p = pn; //申请一个指针
for (; p->next != NULL; p = p->next); //走到链表的尾部
pnewnode->next = p->next; //新结点的指针域指向尾结点的指向
p->next = pnewnode; //尾结点的指针域指向新结点的地址
return true;
}
插入 —— 按位置插
也是先要申请一个指针来替我们走到我们想要的位置的上一个结点,然后申请新结点,新结点指向pos的地址,然后再将上一个结点的指针域指向我们申请的新结点的地址。
//插入——按位置
bool Insert_pos(PNode pn, int pos, ElemType val) {
assert(pn != NULL);
if (pn == NULL) return false;
struct Node* pnewnode = (struct Node*)malloc(1 * sizeof(struct Node));
assert(pnewnode != NULL);
if (pnewnode == NULL) return false;
pnewnode->data = val;
pnewnode->next = NULL;
struct Node* p = pn;
for (int i = 0; i < pos; i++) { //走到想要位置的上一个结点
p = p->next;
}
pnewnode->next = p->next;
p->next = pnewnode;
return true;
}
删除都是要对链表进行判空操作(写在了删除后面)
在设计删除时,有的删除会申请两个指针,是因为一个需要指向/存储待删结点本身的地址,方便删除之后进行释放,不至于造成数据丢失或者内存泄漏,另一个指针则是需要指向待删结点的上一个结点,因为需要该结点来指向待删结点的指针域(可以理解为跳过了待删结点)。
删除——头删
这里不仅要对指针判空,也要对链表进行判空!
先让一个结构体指针保存我们待删除的结点,然后更改头结点的指针域指向,之后再释放我们申请出的结构体指针就好了
//头删
bool Del_head(PNode pn) {
assert(pn != NULL);
if (pn == NULL) return false;
if (IsEmpty(pn)) return false;
struct Node* p = pn->next;
pn->next = p->next;
free(p);
p = NULL;
return true;
}
删除 —— 尾删
在这里我们先申请一个指针,让其走到倒数第二个结点,然后在申请一个结点保存倒数第二个结点的指针域的指向(也就是尾结点),然后再让倒数第二个结点的指向为NULL,之后释放我们第二次申请得到指针。
//尾删
bool Del_tail(PNode pn) {
assert(pn != NULL);
if (pn == NULL) return false;
if (IsEmpty(pn)) return false;
struct Node *p = pn;
while (p->next->next != NULL) {
p = p->next;
}
struct Node* q = p->next;
p->next = NULL;
free(q);
q = NULL;
return true;
}
删除 —— 按位置删
这里我们依然需要两个指针,一个走到待删位置的上一个结点(因为这个结点的指针域指向的是我们待删位置结点的地址),一个用来存储待删结点的地址,以便后面释放。
//按位置删
bool Del_pos(PNode pn, int pos) {
assert(pn != NULL);
if (pn == NULL) return false;
if (IsEmpty(pn)) return false;
struct Node* p = pn;
for (int i = 0; i < pos; i++) { //让指针走到待删位置结点的上一个结点
p = p->next;
}
struct Node* q = p->next; //让指针指待删位置的结点
p->next = q->next;
free(q);
q = NULL;
return false;
}
删除——按值删
调用了查找函数,后面会写到,查找函数返回的是查找值的结点地址。
之后再申请一个指针指向该结点的上一个结点。
//按值删
bool Del_val(PNode pn, ElemType val) {
assert(pn != NULL);
if (pn == NULL) return false;
if (IsEmpty(pn)) return false;
struct Node* p = Search(pn, val); //这里调用了查找函数
if (p == NULL) return false; //返回NULL,说明没找到
//执行下面的就说明找到了待删值所在结点的地址
struct Node* q = pn;
for (; q->next != p; q = q->next); //指针指向待删值结点的上一个结点
q->next = p->next;
free(p);
p = NULL;
return true;
}
判空
直接看头结点的指针域是否为NULL 就行
//判空
bool IsEmpty(PNode pn) {
return pn->next == NULL;
}
查找
通过传值,对链表进行遍历查找,找到了就返还回去,否则就返还NULL
//查找
struct Node* Search(PNode pn, ElemType val) {
assert(pn != NULL);
if (pn == NULL) return NULL;
struct Node* p = pn->next;
for (; p != NULL; p = p->next) { //遍历查找
if (p->data == val) {
return p; //找到,返回结点
}
}
return NULL;
}
获取有效个数
//获取有效个数
int Get_length(PNode pn) {
int count = 0;
struct Node* p = pn->next;
for (; p != NULL; p = p->next) {
count++;
}
return count;
}
清空
调用销毁函数就好
//清空
void Clear(PNode pn) {
Destroy1(pn); //调用销毁函数就好
}
销毁1——使用头结点
//销毁1 无限头删
void Destroy1(PNode pn) {
while (pn->next != NULL) {
struct Node* p = pn->next;
pn->next = p->next;
free(p);
p = NULL;
}
}
销毁2 —— 不使用头结点
使用两个指针来进行销毁,只要p不等于NULL,那么就让q指向p->next,并把p释放了,在让p=q。
//销毁2
void Destroy2(PNode pn) {
assert(pn != NULL);
if (pn == NULL) return;
struct Node* p = pn->next;
struct Node* q;
pn->next = NULL;
while (p != NULL)
{
q = p->next;
free(p);
p = q;
}
}
打印
//打印
void Show(PNode pn) {
for (struct Node* p = pn->next; p != NULL; p = p->next)
{
printf("%d ", p->data);
}
printf("\n");
}
测试用例:
int main() {
struct Node head;
Init_List(&head);
for(int i=0; i<20; i++)
{
Insert_pos(&head, i, i+1);
}
Show(&head);
Insert_head(&head, 100);
Insert_tail(&head, 200);
Show(&head);
printf("------------------->\n");
Del_head(&head);
Del_tail(&head);
Show(&head);
Del_pos(&head, 4);
Show(&head);
Del_val(&head, 14);
Show(&head);
printf("length = %d\n", Get_length(&head));
Destroy1(&head);
//Destroy2(&head);
Show(&head);
return 0;
}