1.顺序表对比单链表的缺点
- 中间或头部插入时,需要移动数据再插入,如果数据庞大会导致效率降低
- 每次增容就需要申请空间,而且需要拷贝数据,释放旧空间
- 增容造成浪费,因为一般都是以2倍增容
2.链表的基础知识
- 链表也是线性表的一种。物理结构:不一定线性,逻辑结构:一定是线性的
- 链表物理结构也不是线性的。链表由一个一个的节点组成
- 节点由 :数据和指向下一个地方的指针。每一个节点都会存储下一个节点的地址
- List 表示链表, S表示single ,Node表示节点
2.1链表基本结构
cpp
typedef int SLDataType;//节点类型,S 表示节点,L表示链表
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLNode;
3.代码实现
- 设置三个文件,SList.h 头文件 SList.c 功能实现文件,test.c测试文件
- 声明写在头文件里面 SList.h
cpp
#pragma once
#include <stdio.h>
#include <stdlib.h>//用一个就申请一个空间
#include <assert.h>
typedef int SLDataType;
typedef struct SListNode
{
int data;
struct SListNode* next;
}SLNode;
//链表打印
void SLPrint(SLNode* phead);
3.1.实现思路
- 为两个节点开辟空间,并通过赋值给他们链接起来,通过节点内的地址
- 通过传过来的头节点去找尾节点,从而达到遍历链表
- 通过改变pcur的指向,相当于可以访问下一块空间了
cpp
void SLPrint(SLNode* phead)//phead 表示头节点
{
SLNode* pcur = phead;//pcur 临时的节点
while (pcur != 0)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
3.1.1测试方法
- 开辟了空间并且把他们都串起来。
cpp
void SListTest1()
{
SLNode* Node1 = (SLNode*)malloc(sizeof(SLNode));//初始化
Node1->data = 1;
SLNode* Node2 = (SLNode*)malloc(sizeof(SLNode));
Node2->data = 2;
SLNode* Node3 = (SLNode*)malloc(sizeof(SLNode));
Node3->data = 3;
Node1->next = Node2;
Node2->next = Node3;
Node3=->next = NULL;//最后一个置为NULL
//打印链表
SLPrint(Node1);
}
3.1.2当while循环条件不一样时
- 这里用图说明问题,当pcur 和pcur->next 。pcur不为NULL,还会多走一步。
- pucr->next 为空不往下走,就是说,pcur 比pcur->next,多走一步
4.0尾插
- 在尾插开始之前,需要判断两种情况
- 第一种就是空链表和非空链表,如果为空链表,肯定要开辟空间的
- 在这个新开辟的空间里面,顺便一起把需要插入的数据放这里一起插入,也就说涉及到插入会对空间进行增加的就要新空间
- 开辟好了空间,判断有效性,然后放入数据,再给个NULL
cpp
//新空间
SLNode* SLBuyNode(SLDataType x)
{
SLNode* Newnode = (SLNode*)malloc(sizeof(SLNode));
if (Newnode == NULL)
{
perror("malloc New");
exit(1);//非0表示异常返回,会导致直接跳出整个程序
}
//开辟完新空间记得放入数据
Newnode->data = x;
Newnode->next = NULL;
return Newnode;//开辟完空间要记得返回
}
- 如果不是空链表,就要通过头结点找到尾节点,一定是要找到存放下一个节点的next
- 找了尾节点,就可以直接在尾节点放上新开辟好,因为对应数据已经在新空间里弄好了
cpp
//链表尾插
void SLPushBack(SLNode** pphead, SLDataType x)
{
assert(pphead);//这里不能判断*pphead,因为有可能本来就是空链表
//尾插的时候需要看是否为NULL,空链表和非空链表
SLNode* Newnode = SLBuyNode(x);//把需要插入的值传过去
if (*pphead == NULL)
{
*pphead = Newnode;//如果是NULL就把新开辟的给到头节点
}
else
{
SLNode* ptail = *pphead;
while (ptail->next != NULL)//查找尾节点
{
ptail = ptail->next;
}
ptail->next = Newnode;//新节点里面已经置为空指针
}
}
4.1测试方法
- 往后的测试方法都不写了,学会了后根据对应参数测试就OK了。
- 这里注意一定要传二级指针的地址,对一级指针改变指向就是要传二级指针
- **举个例子,**看下面一个传址调用,另外一个是传值调用
cpp
SListTest2()
{
SLNode* node = NULL;//新创建的节点要初始化
//测试尾插
//SLPushBack(&node, 2);
//SLPushBack(&node, 3);
//SLPushBack(&node, 4);
//SLPrint(node);
//测试头插
SLPushFront(&node, 4);
SLPushFront(&node, 3);
SLPushFront(&node, 2);
SLPushFront(&node, 1);
SLPrint(node);
}
1.可以看上面你要&node的地址才能对node改变,而不是他创建node,而你就传node,这个就和传值调用不就一样了吗?
5.0头插
- assert(pphead);//这里不能判断*pphead,因为有可能本来就是空链表
- 头插当然就很简单了,直接让新开辟的空间指向头节点
- 然后把头结点权限给新空间,让他newnode来当第一个
cpp
//链表头插
void SLPushFront(SLNode** pphead, SLDataType x)
{
assert(pphead);//这里不能判断*pphead,因为有可能本来就是空链表
SLNode* newnode = SLBuyNode(x);
newnode->next = *pphead;//把之前的头节点给到新节点
*pphead = newnode;//头插成为新节点
}
6.0尾删
- 第一种:如果是一个节点就可以直接删除
- 第二种:在进行尾删的时候也要找尾,而且还有保留前一个节点的地址
- 然后保留的前一个节点的地址,他指向的下一个地址就可以置为NULL
cpp
//链表尾删
void SLDelBack(SLNode** pphead)
{
//传过来的地址必须有效
assert(pphead && *pphead);
//首先要考虑2种情况,有链表的情况,一个节点的情况
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLNode* ptail = *pphead;//再创建一个临时的指针更好,可以避免优先级问题,还有能保留源地址
SLNode* pcur = *pphead;
while (ptail->next)
{
pcur = ptail;//ptail指向前的一个节点
ptail = ptail->next;
}
free(ptail);//直接free ptail
ptail = NULL;
pcur->next = NULL;
}
}
6.1尾删最后一步,倒推图
7.头删
- 链表不能为空,为NULL删什么啊,当然对应的二级指针也不能为空
- -> 符号优先级 比 * 星号高
- 两个问题点
- 一个节点:这个问题怎么提出的因为链表为空不能删会报错,那么本身方法无意义了
- 多个节点:先把指向的节点给到*pphead,也就是我们的头节点,然后再删除之前的头节点
- 头删的时候要把这个节点删除,但是我们要能找到下一个节点并成为头节点
- 由此可以得出先把next = Node1->next。不能直接给赋值给*pphead,因为后面还要删除头节点,所以我们先保存下一个节点的地址
cpp
void SLDelFront(SLNode** pphead)
{
assert(pphead && *pphead);//链表为空就没有删除的意义了
SLNode* next = (*pphead)->next;//保存下一个节点
free(*pphead);
*pphead = next;
}
8.链表查找节点
- 在查找链表的时候,你可能要想想,指针走到后面的NULL,万一后面还有需要查找的值呢?所以还要备份一个源数据
- 遍历向后查找,找到返回这个节点。
- 这个查找代码就是向后遍历,如果相等就返回,否则返回NULL
cpp
SLNode* SLFind(SLNode* phead, SLDataType x)
{
assert(phead);//等价与 phead != NULL
SLNode* pcur = phead;//可能需要多次查找
while (pcur)//结束调试是最后一个节点的后一个NULL,往下查找无意义
{
if (pcur->data == x)
{
return pcur;//如果到了当前节点,就找到当前节点下面的值
}
pcur = pcur->next;//访问到当前节点指向的下一个,没有向下查找条件会死循环
}
return NULL;
}
9.指定位置之前插入
- insert 插入 after 在什么之后 prev 在什么之前,对数据增加和删除,就需要二级指针
- 链表的二级指针不能为空,而且链表也不能为空 因为要pos要通过链表找打指定位置,当然pos也不能为NULL,在NULL的位置插入吗
- prev需要找到pos之前的位置while(prev->next ! = 0),没有找到就继续往下走
- prev 和 pos都找到了,那么要在两者之前插入
- 注意: 关于pos参数,是Find查找到前的,因为find是查找函数,找到指定位置然后返回
cpp
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
assert(pphead && *pphead);
assert(pos);//不能为空,为你还找啥呢?
if (pos == *pphead)
{
SLPushFront(pphead, x);
}
else
{
SLNode* prev = *pphead;//prev 表示 pos的前一个节点
SLNode* newnode = SLBuyNode(x);
while (prev->next != pos)//如过指向的下一个节点不是pos,则继续往下找
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
10.在指定位置之后插入数据
- 从图中可以看出正确的方式是的二种,第一种导致找不到pos->next的节点
- 在指定位置之后不需要第一个有效节点,用不到第一个节点,通过pos就可以找到后面一个节点
cpp
//在指定位置之后插入
void SLInsetAfter(SLNode* pos, SLDataType x)
{
assert(pos);//指定为空,还怎么删除
SLNode* newnode = SLBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;//pos->next 原先指向的数据已经给到newnode->next
}
11.pos位置删除
- 首先就是要断言,传过来的二级指针里面装着的链表,当然都不能为空,pos节点为空了就说明指定位置没有该数据
- 情况1:pos不是第一个有效节点的位置
- 情况2:pos是第一个有效节点的位置
cpp
//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos)
{
assert(pphead && *pphead);
assert(pos);
//两种情况:1.pos节点没有指向第一个有效节点,反之就是指向了
if (pos == *pphead)
{
//相当于头删了
SLDelFront(pphead);
}
else
{
SLNode* prev = *pphead;
while (prev->next != pos)//他指向的节点,等于了pos就相当于找到了前一个节点
{
prev = prev->next;//通过当前的找到下一个,直到找到pos
}
//prev -> pos -> pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
12.pos之后的节点删除
- pos和pos->next,的节点都不能为NULL,下一个节点为NULL你删除什么?
- 在删除pos之后的节点之前,我们要先保存pos->next这个节点,防止内存泄漏,创建一个临时的del 变量来保存
cpp
//删除pos节点之后的
void SLEraseAfter(SLNode* pos)
{
assert(pos && pos->next);//都不能为空
SLNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
13.销毁节点
- 销毁节点需要一个一个的销毁
- 大致的思路是
- 先保存下一个节点的地址,然后再释放这个节点
- 释放完后,把下一个节点的地址给pcur,从而构成循环
- 直到为NULL为止
cpp
//销毁链表
void SLDesTroy(SLNode** pphead)
{
SLNode* pcur = *pphead;
while (pcur)
{
SLNode* del = pcur->next;//在销毁之前保存下一个节点的地址
free(pcur);
pcur = del;
}
pcur = NULL;
}
4.最后的完整代码
4.1头文件部分
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int SLDataType;
typedef struct SListNode
{
SLDataType val;
struct SListNode* next;
}SLNode;
//链表打印
void SLPrint(SLNode* phead);
//链表尾插和头插
void SLPushBack(SLNode** pphead, SLDataType x);
void SLPushFront(SLNode** pphead, SLDataType x);
//链表头删和尾删
void SLDelBack(SLNode** pphead);
void SLDelFront(SLNode** pphead);
//链表查找
SLNode* SLFind(SLNode* phead, SLDataType x);
//链表pos之前插入
void SLInset(SLNode** pphead, SLNode* pos, SLDataType x);
//链表pos之后插入
void SLInsetAfter(SLNode* pos, SLDataType x);
//链表pos节点删除
void SLErase(SLNode** pphead,SLNode* pos);
//链表pos之后删除
void SLEraseAfter(SLNode* pos);
//链表的销毁
void SLDesTroy(SLNode* pphead);
4.2方法部分
cpp
#include "SList.h"
void SLPrint(SLNode* phead)
{
SLNode* pcur = phead;
while (pcur)//如果是pcur->next为结束条件会导致,提前结束不进入循环
{
printf("%d->", pcur->val);
pcur = pcur->next;
}
printf("NULL\n");
}
//新节点空间
SLNode* SLBuyNode(SLDataType x)
{
SLNode* Newnode = (SLNode*)malloc(sizeof(SLNode));
if (Newnode == NULL)
{
perror("malloc new");
exit(1);//异常返回退出整个工程
}
Newnode->val = x;
Newnode->next = NULL;
return Newnode;
}
//尾插
void SLPushBack(SLNode** pphead, SLDataType x)
{
assert(pphead);
SLNode* newnode = SLBuyNode(x);
//空链表和非空链表
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLNode* ptail = *pphead;
while (ptail->next != NULL)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
//头插
void SLPushFront(SLNode** pphead, SLDataType x)
{
assert(pphead);
SLNode* newnode = SLBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;//直接把第一个节点的位置给newnode,成为新的头结点
}
//尾删
void SLDelBack(SLNode** pphead)
{
assert(pphead && *pphead);
//一个节点的情况
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLNode* prev = *pphead;//保留的前一个节点
SLNode* ptail = *pphead;
while (ptail->next != NULL)//跳出循环就表示找到前一个节点了
{
prev = ptail;//保留了结束前的前一个地址
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
prev->next = NULL;//指向的下一个节点为NU
}
}
//头删
void SLDelFront(SLNode** pphead)
{
assert(pphead && *pphead);
SLNode* pcur = (*pphead)->next;
free(*pphead);
*pphead = pcur;
}
//查找
SLNode* SLFind(SLNode* phead, SLDataType x)
{
assert(phead);
SLNode* pcur = phead;
while (pcur)
{
if (pcur->val == x)
return pcur;
pcur = pcur->next;
}
return NULL;
}
//pos位置之前插入
void SLInset(SLNode** pphead, SLNode* pos, SLDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLNode* newnode = SLBuyNode(x);
if (*pphead == pos)//因为下面一种会找不到
{
SLPushFront(pphead, x);//这里要传二级指针因为,要对链表进行改变
}
else
{
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//perv pos
newnode->next = pos;
prev->next = newnode;
}
}
//pos之后插入
void SLInsetAfter(SLNode* pos, SLDataType x)
{
assert(pos);
SLNode* newnode = SLBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//pos位置处删除
void SLErase(SLNode** pphead, SLNode* pos)
{
assert(pos);
if (*pphead == pos)//调用头删
{
SLDelFront(pphead);
}
else
{
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//pos之后删除
void SLEraseAfter(SLNode* pos)
{
assert(pos && pos->next);//pos后面的一个节点也不能为NULL
SLNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
//销毁链表
void SLDesTroy(SLNode** pphead)
{
SLNode* pcur = *pphead;
while (pcur)
{
SLNode* del = pcur->next;//在销毁之前保存下一个节点的地址
free(pcur);
pcur = del;
}
pcur = NULL;
}
4.3测试部分
cpp
#include "SList.h"
SListTest1()
{
SLNode* node = NULL;//新创建的节点要初始化
//测试尾插
//SLPushBack(&node, 2);
//SLPushBack(&node, 3);
//SLPushBack(&node, 4);
//SLPrint(node);
//测试头插
SLPushFront(&node, 4);
SLPushFront(&node, 3);
SLPushFront(&node, 2);
SLPushFront(&node, 1);
SLPrint(node);
测试尾删
//SLDelBack(&node);
//SLDelBack(&node);
//SLDelBack(&node);
//SLDelBack(&node);
//SLPrint(node);
测试头删
//SLDelFront(&node);
//SLDelFront(&node);
//SLDelFront(&node);
//SLDelFront(&node);
//SLDelFront(&node);
//SLPrint(node);
//测试查找
SLNode* find = SLFind(node,3);
if (find)//非0值,NULL其实原本意思也是0
printf("找到了\n");
else
printf("没找到\n");
//pos前插入
//SLInset(&node, find, 11);
//SLPrint(node);
//pos后插入
//SLInsetAfter(find, 22);
//SLPrint(node);
//删除pos位置
//SLErase(&node, find);
//SLPrint(node);
//删除pos位置之后的
SLEraseAfter(find);
SLPrint(node);
SLDesTroy(&node);
}
int main()
{
SListTest1();
return 0;
}
总结:
- 做代码题的时候,把可能发生情况按照步骤写清楚,分为几点等等
- 最好把每个情况里的推导步骤也要写清楚