单链表
概念
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链表次序实现的

打印单链表
// 打印链表
void SLTPrint(SLTNode* phead) // Phead指向第一个结点数据
{
// 这里可以不可以短语assert(Phead)???
// 空链表可以打印数据,但是没有输出数据
SLTNode* cur = phead;
while (cur != NULL)
{
// cur->data代表查找cur当前的data数据
printf("%d->", cur->data);
// cur->next指向下一个链表
cur = cur->next;
}
printf("NULL\n");
}
- phead指向第一个结点
- assert(phead);断言没有必要,phead可以指向NULL,说明phead指向的链表为空
- 链表向后移动的时候,需要使用当前结构体对象的指针,这个指针会指向下一个结构体对象,以此来查找单链表数据。

理解单链表

- 链表的物理结构是每一个结构体中都存储下一个结构体对象的首元素的地址,所以链表之间不是连续的。
- 在物理结构中每一个结点都是在堆区申请出来的,这个地址是随机的,在内存空间中不一定连续。【注意】上图中的地址仅为举例,不一定连续
- 在实际进行理解链表数据结构和进行算法题时,仅仅需要构图出逻辑结构即可。

单链表尾部插入数据
// 尾插数据
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
// 构造新结点
SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
if (newNode == NULL)
{
perror("malloc fail!\n");
return;
}
newNode->data = x;
newNode->next = NULL;
if (*pphead == NULL)
{
*pphead = newNode;
}
else
{
// 链接尾部结点
SLTNode* tail = *pphead;
while (tail->next != NULL)//注意尾部结点是尾部的结点下一个为空,不能是此结点为空
{
tail = tail->next;
}
tail->next = newNode; // 链接
}
}
- 单链表插入数据不会考虑是否内存是否足够,因为每一次添加数据都会在堆区进行申请内容。
- 尾部插入数据需要在最后一个结点(而非最后一个NULL)的地方,链接下一个结点
- 尾部掺入数据的原理是,在尾部最后一个结点的指针上存储新结点的地址,以便查找新结点。
- 注意phead为空的情况,只需要将新申请的结点链接在phead的后面。

【注意】这里传参的时候,实参应该传递地址,形参应该是二级指针。如果调用函数的时候是SLTPushBack(plist, 1);这里调用的是一级指针,如左图所示。plist指向一块地址,传递给形参(形参如果是SLTNode* pphead)的时候,phead也会同样指向这块地址,当申请新地址的时候,phead会指向新申请的地址,而不是plist指向这块新地址,在函数调用结束之后,phead在栈区吗,会被释放,而phead原本指向的新地,在堆区,会造成内存泄漏。;
而传递二级指针,SLTPushBack(&plist, 1);phead会指向plist,形参是SLTNode** pphead,申请空间后,将*phead指向新地址,会让plist指向新地址。

这里需要对虚拟内存有一定的了解,也就是堆区和栈区如何分布。C语言最难的部分就是指针,如何理解指针,就需要理解指针所指向的那一块地址到底是什么!!!改变实参,就需要传递实参的指针,形参就需要在实参的基础上加一层指针。
单链表添加新结点
SLTNode* SLTNewNode(SLTDataType x)
{
SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
if (newNode == NULL)
{
perror("malloc fail!");
return NULL;
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
由于会经常遇到插入新结点的函数,可以将申请新结点包装成一个函数,以便后续使用。
单链表头部插入数据
// 头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newNode = SLTNewNode(x);
if (newNode == NULL)
{
return;
}
newNode->next = *pphead;
*pphead = newNode;
}
在单链表头部插入数据不需要考虑phead的指向的内容为空,这种情况也被上述代码包含在内。

单链表尾部删除数据
// 尾删
void SLTPopBack(SLTNode** pphead)
{
SLTNode* tail = *pphead;
if (tail == NULL)
{
// 没有结点
return;
}
if (tail->next == NULL)
{
// 只有一个结点
free(tail);
*pphead = NULL;
}
else
{
// 为了防止释放最后一个结点后,上一个结点的next指向的内容为野指针
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
- 尾部数据删除也需要二级指针,同样这是在修改实参的对象。
- 如果链表删除的时候,需要考虑只剩下一个结点和没有结点的情况
- 在删除最后一个结点的时候,需要保存上一个结点的指针,以便将上一个指针的next置为NULL
单链表头部删除数据
// 头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
SLTNode* head = *pphead;
if (head == NULL)
{
// 没有结点
return;
}
// 有一个结点或者多个结点
*pphead = head->next;
free(head);
head = NULL;
}
【注意】头删只需要考虑没有结点或者有结点的情况,同时给pphead赋值指针的时候,需要给pphead所指地址的元素赋值,而不是pphead赋值地址。

单链表查找元素
// 查找元素
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x)
{
// 链表为空,断言无法查找
// aseert(*pphead);
// 也可以返回空指针
if (*pphead == NULL)
{
return NULL;
}
SLTNode* find = *pphead;
while (find != NULL)
{
if (find->data == x)
{
return find;
}
find = find->next;
}
// 查找整个链表没有找到,返回NULL
if (find == NULL)
{
return NULL;
}
}
- 查找元素注意链表为空
- 其次,需要遍历全部的链表,直到最后一个链表的后面
单链表在pos位置的前面插入元素
// 链表在pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(phead);
// 如果传输的内容为空
if (pos == NULL)
{
return;
}
if (pos == *pphead)
{
// 头插
SLTPushFront(pphead, x);
}
else
{
// 找到pos的前一个位置
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
if (prev->next == NULL)
{
//prev走到最后一个结点还没有发现pos结点的存在
printf("Do not find pos\n");
return;
}
}
// 制造一个新结点
SLTNode* newNode = SLTNewNode(x);
if (newNode == NULL)
{
return;
}
// 链接
prev->next = newNode;
newNode->next = pos;
}
}

【注意】断言pphead,而不是*pphead,说明pphead所指向的内容不能为空,而*pphead所指向的内容是链表,链表可以为空。
- pphead存放的是指向链表首元素的地址的指针,不可以为空
- *pphead就是解引用pphead这个指针了,此时*pphead就是plist,而plist可以存放结点,也可以不存放结点
- 只要是形参是**phead,都需要对pphead进行断言;而只有再删除结点的时候,才需要对*pphead进行断言,这是因为*pphead必须有结点,没有结点就无法进行删除。
单链表pos位置删除元素
// 链表pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead); // pphead没有存放指向链表首元素的指针
assert(*pphead); // 链表为空,不可以删除
assert(pos); // 防止传递的pos位置为空
if (*pphead == pos)
{
// 如果pos指的是第一个结点
// 头删
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
通过分析单链表在pos位置之前插入,或者pos位置删除,都i需要遍历全部数组,提高时间复杂度。
单链表pos位置后面插入元素
// 链表在pos位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
// 制造一个新结点
SLTNode* newNode = SLTNewNode(x);
if (newNode == NULL)
{
return;
}
// 下面的顺序不能翻转
newNode->next = pos->next;
pos->next = newNode;
}
newNode的下一个结点必须是pos的下一个结点,然后将pos的下一个结点的换成newNode。
单链表pos后面删除元素
// 链表pos位置之后删除
void SLTEraseAfter(SLTNode* pos)
{
// 至少有两个结点
assert(pos);
assert(pos->next);
// 先保存指向pos->next的结点
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
}
单链表优点和缺点
对比单链表所有的删除元素和添加元素发现:
优点:
- 在某个结点后面插入或者删除结点,非常迅速,时间复杂度为O(1)
- 头部插入结点,头部删除结点,非常迅速,时间复杂度为O(1)
- 内存分配灵活,无需预先分配连续空间,按需申请节点
- 存储空间利用率高
缺点:
- 只提供头结点,删除最后一个结点或者在尾部插入数据,时间复杂度为O(N)
- 查找元素需要遍历全部的结点,时间复杂度为O(N)
- 在某个结点之前删除结点或者添加结点,时间复杂度为O(N)
代码完整实现
SList.h头文件
#ifndef SLIST_H_
#define SLIST_H_
#include<stdio.h>
#include<stdlib.h>
typedef int SLTDataType;
typedef struct SListNode
{
// 结构体中保存的数据
SLTDataType data;
// 结构体中有一个结构体指针,指针指向下一个结构体
struct SListNode* next;
}SLTNode;
// 打印链表
void SLTPrint(SLTNode* Phead);
// 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
// 头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
// 尾删
void SLTPopBack(SLTNode** pphead);
// 头删
void SLTPopFront(SLTNode** pphead);
// 添加结点
SLTNode* SLTNewNode(SLTDataType x);
// 查找元素
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x);
// 链表在pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 链表pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos);
// 链表在pos位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
// 链表pos位置之后删除
void SLTEraseAfter(SLTNode* pos);
#endif
SList.c源文件
#include"SList.h"
// 打印链表
void SLTPrint(SLTNode* phead) // Phead指向第一个结点数据
{
// 这里可以不可以短语assert(Phead)???
// 空链表可以打印数据,但是没有输出数据
SLTNode* cur = phead;
while (cur != NULL)
{
// cur->data代表查找cur当前的data数据
printf("%d->", cur->data);
// cur->next指向下一个链表
cur = cur->next;
}
printf("NULL\n");
}
// 尾插数据
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
// 构造新结点
SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
if (newNode == NULL)
{
perror("malloc fail!\n");
return;
}
newNode->data = x;
newNode->next = NULL;
if (*pphead == NULL)
{
*pphead = newNode;
}
else
{
// 链接尾部结点
SLTNode* tail = *pphead;
while (tail->next != NULL)//注意尾部结点是尾部的结点下一个为空,不能是此结点为空
{
tail = tail->next;
}
tail->next = newNode; // 链接
}
}
SLTNode* SLTNewNode(SLTDataType x)
{
SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
if (newNode == NULL)
{
perror("malloc fail!");
return NULL;
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
// 头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newNode = SLTNewNode(x);
if (newNode == NULL)
{
return;
}
newNode->next = *pphead;
*pphead = newNode;
}
// 尾删
void SLTPopBack(SLTNode** pphead)
{
SLTNode* tail = *pphead;
if (tail == NULL)
{
// 没有结点
return;
}
if (tail->next == NULL)
{
// 只有一个结点
free(tail);
*pphead = NULL;
}
else
{
// 为了防止释放最后一个结点后,上一个结点的next指向的内容为野指针
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
// 头删
void SLTPopFront(SLTNode** pphead)
{
SLTNode* head = *pphead;
if (head == NULL)
{
// 没有结点
return;
}
// 有一个结点或者多个结点
*pphead = head->next;
free(head);
head = NULL;
}
// 查找元素
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x)
{
// 链表为空,断言无法查找
// aseert(*pphead);
// 也可以返回空指针
if (*pphead == NULL)
{
return NULL;
}
SLTNode* find = *pphead;
while (find != NULL)
{
if (find->data == x)
{
return find;
}
find = find->next;
}
// 查找整个链表没有找到,返回NULL
if (find == NULL)
{
return NULL;
}
return NULL;
}
// 链表在pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(*pphead);
// 如果传输的内容为空
if (pos == NULL )
{
return;
}
if (pos == *pphead)
{
// 头插
SLTPushFront(pphead, x);
}
else
{
// 找到pos的前一个位置
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
if (prev->next == NULL)
{
//prev走到最后一个结点还没有发现pos结点的存在
printf("Do not find pos\n");
return;
}
}
// 制造一个新结点
SLTNode* newNode = SLTNewNode(x);
if (newNode == NULL)
{
return;
}
// 链接
prev->next = newNode;
newNode->next = pos;
}
}
// 链表pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead); // pphead没有存放指向链表首元素的指针
assert(*pphead); // 链表为空,不可以删除
assert(pos); // 防止传递的pos位置为空
if (*pphead == pos)
{
// 如果pos指的是第一个结点
// 头删
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
// 链表在pos位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
// 制造一个新结点
SLTNode* newNode = SLTNewNode(x);
if (newNode == NULL)
{
return;
}
// 下面的顺序不能翻转
newNode->next = pos->next;
pos->next = newNode;
}
// 链表pos位置之后删除
void SLTEraseAfter(SLTNode* pos)
{
// 至少有两个结点
assert(pos);
assert(pos->next);
// 先保存指向pos->next的结点
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
}



被折叠的 条评论
为什么被折叠?



