目录
- 前置思考
- 3.链表
- 3.1 链表的概念及结构
- 3.2 链表的分类
- 3.3 无头+单向+非循环链表增删查改接口实现
- 3.4 链表面试题
- 1. 删除链表中等于给定值 val 的所有节点。
- 2. 反转一个单链表。
- 3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
- 4. 输入一个链表,输出该链表中倒数第k个结点。
- 5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
- 6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。
- 7. 链表的回文结构。
- 8. 输入两个链表,找出它们的第一个公共结点。
- 9. 给定一个链表,判断链表中是否有环。
- 10. 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
- 11. 给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。要求返回这个链表的深度拷贝。
- 12. 其他 。
学习链表之前,建议先学习下顺序表,这是博主的顺序表文章,欢迎学习初阶数据结构(C语言实现)——3顺序表
前置思考
为什么有了顺序表,还要有链表?
- 顺序表存在一些问题
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们
再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
- 为了更好的解决上面的问题,我们引入了链表
3.链表
3.1 链表的概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
链表的物理结构
1.从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
链表的逻辑结构(为了方便理解,想象出来的)
注意:
- 现实中的结点一般都是从堆上申请出来的
- 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
3.2 链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
- 单向或者双向
- 带头或者不带头(哨兵位的头结点,不存储有效数据)
- 循环或者非循环
链表总共有8种
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
3.3 无头+单向+非循环链表增删查改接口实现
3.3.0 单链表的实现
typedef int SLTDateType;
//定义单链表节点
typedef struct SListNode
{
SLTDateType data; //数据域
struct SListNode* next;//指针域
}SListNode;//重命名为SListNode,方便我们后面定义结构体变量。就不需要每次写 struct SListNode 这么一长串了
- 实现接口
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* phead);
// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pphead, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pphead);
// 单链表头删
void SListPopFront(SListNode** pphead);
// 单链表查找
SListNode* SListFind(SListNode* phead, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
3.3.1 动态申请节点和释放销毁节点
动态申请一个结点
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)//申请完,检查是否开辟成功
{
perror("BuySListNode::mallo fail!");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
释放(销毁)所有节点
//销毁(释放)所有节点
void SLListDistory(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
while (cur->next != NULL)//遍历链表
{
SListNode* next = cur->next;//保存cur的下一个节点
free(cur);//释放结点
cur = next;//cur一直往后走
}
*pphead = NULL;//最后释放我们的指针
}
3.3.2 单链表打印
void SListPrint(SListNode* phead)
{
SListNode *cur = phead;
while (cur)//遍历链表只要不为空,就打印
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
3.3.3 单链表尾插
- 先来看一种错误写法
传一级指针的值,用一级指针接收
- 指针传值,相当于把plist指针变量的值拷贝给phead,phead=newnode,phead的变化并不会影响plist.
- 形参只是实参的一份临时拷贝
- 这样写的结果是什么?
当链表为空的时候,plist指向NULL,phead也指向NULL
我们现在要尾插一个结点,我们在函数内部,令形参phead指向了新结点,但是我们的实参plist,并没有改变
- 如何解决
list 是指向第一个节点的指针,想要在函数中改变 plist 的值(指向),必须要把 plist指针的地址 作为实参传过去,形参用 二级指针 接收,这样在函数中对二级指针解引用得到 plist 的值,就可以改变 plist 的值了
在函数里面要改变 int,则要传 int* ,要改变 int* ,则要传 int**
- 尾插正确写法
图解尾插思路>
void SListPushBack(SListNode** pphead, SLTDateType x)
{
assert(pphead); //检查参数是否传错
SListNode* newnode = BuySListNode(x);//动态申请一个结点
if (*pphead == NULL)//当单链表中没有结点时
{
*pphead = newnode;//让plist指向新结点
}
else//当单链表中已有结点时
{
SListNode* tail =*pphead;
while (tail->next != NULL)//找尾,找到单链表中的最后一个结点
{
tail = tail->next;
}
tail->next = newnode;//另最后一个结点的next域指向新结点
}
}
- 功能测试
3.3.4 单链表的头插
- 图解头插思路
- 代码实现
void SListPushFront(SListNode** pphead, SLTDateType x)
{
assert(pphead); //检查参数是否传错
SListNode* newnode = BuySListNode(x);//动态申请一个节点
newnode->next = *pphead; //新节点的next指针指向plist指向的位置
*pphead = newnode;//plist指向头插的新节点
}
- 功能测试
3.3.5 单链表的尾删
- 图解尾删思路
- 尾删代码实现
- 单链表只有一个节点时,删除节点,plist 指向 NULL;
- 单链表有多个节点时,先找到单链表尾节点的上一个节点,删除尾节点,然后将该节点的next指向 NULL;
- 因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。
void SListPopBack(SListNode** pphead)
{
assert(pphead); //检查参数是否传错
assert(*pphead); //断言,链表不能为空
// 1、只有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);//删除节点
*pphead = NULL;//plist置空
}
else
{
// 2、多个节点
//找尾
SListNode* prev = NULL;
SListNode* tail = *pphead;
while (tail->next != NULL)//找到链表的尾节点和它的上一个节点
{
prev = tail;
tail = tail->next;
}
free(tail);//删除尾节点
tail = NULL;
prev->next = NULL;//置空
}
}
- 尾删功能验证
3.3.6 单链表头删
- 图解单链表头删思路
- 单链表头删代码实现
void SListPopFront(SListNode** pphead)
{
assert(pphead); //检查参数是否传错
assert(*pphead); //链表不能为空
SListNode* first = *pphead; //保存头节点的地址
*pphead = first->next;//plist指向头节点的下一个节点
free(first); //删除头节点
first = NULL;
}
- 单链表头删功能验证
3.3.7 单链表查找
- 思路比较简单,如果找到就返回找到的结点,没找到就返回NULL
SListNode* SListFind(SListNode* phead, SLTDateType x)
{
SListNode* cur = phead;
//遍历链表
while (cur)
{
if (cur->data == x)
{
return cur;//找到了,返回该节点的地址
}
cur = cur->next;
}
return NULL; //未找到,返回NULL
}
- 查找函数功能验证
3.3.8 单链表在pos位置之后插入x
- 图解单链表在pos位置之后插入思路
- 思考:为什么不在pos位置之前插入?
- 单链表不适合在pos位置之前插入,因为需要遍历链表找到pos位置的前一个节点
- 单链表更适合在pos位置之后插入,如果在后面插入,只需要知道pos位置就行,会简单很多
- C++官方库里面单链表给的也是在之后插入
- 代码实现
//单链表在指定pos位置之后插入
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
assert(pos);//给的pos位置不能为空
SListNode* newnode = BuySListNode(x);//动态申请一个节点
newnode->next = pos->next;//新节点的next指针指向pos位置后一个节点
pos->next = newnode; //pos位置的next指向新节点
}
3.3.9 单链表删除pos位置之后的值
//单链表删除指定pos位置之后的节点
void SListEraseAfter(SListNode* pos)
{
assert(pos);//给的pos位置不能为空
assert(pos->next); //给的pos位置不能是尾节点
SListNode* del = pos->next;//保存pos位置的后一个节点
pos->next = del->next;
free(del); //释放pos位置的后一个节点
del = NULL;
}
3.3.10 求单链表长度
//求单链表长度
int SListSize(SListNode* phead)
{
int size = 0;
SListNode* cur = phead;
while (cur != NULL) //遍历链表
{
size++;
cur = cur->next;
}
return size;
}
3.3.11 判断单链表是否为空
//单链表判空
bool SListEmpty(SListNode* phead)
{
//plist为空,返回1(true),非空,返回0(false)
return phead == NULL;
/*写法2:
return phead == NULL ? true : false;
*/
}