前言
给爷冲。之后还会有带头结点的。
一 单链表的存储结构
单链表存储结构最重要的就是数据域和指针域了,我们定义了一个额外的link 类型,其实就是LinkNode*类型,为了之后链表操作不用写二级指针,比较方便。当然,你不定义Link类型也可以。本质都一样。有些小伙伴不理解为什么单链表的操作(比如插入节点操作)要用二级指针?(c语言实现的基础上才有指针玩法)。
就这么简单和你说吧,我们在单链表用的指针用法,大多数都是在形参列表时候会用到,你就简单理解,因为你需要改变单链表里面的 next指针(一级),所以要传二级指针,就像你要改变一个变量一样(类比于0级指针),你需要传一级指针去改变这个变量。简单的理解,你需要改变多少级的变量,你就设计比它高一级的指针为形参就可以。先理解这一点很重要。
#define ELemType int
typedef struct LinkNode
{
ELemType date; //数据域
struct LinkNode* next; //指针域
}LinkNode;
typedef LinkNode* Link;
由于单链表最最最常用的操作也就是尾插尾删头插头删了,所以掌握这几点就比较重要咯.
二 常用的接口实现
初始化
void InitLink(Link* head)
{
*head = NULL;
}
初始化时候直接给链表赋值为空就可以,表示为空表。我手画了一个理解。
在vs2013平台的调试监视的结果:
1.尾插
尾插可是有好多细节呀!(因为指针,哎,就很容易出事)可要好好看看!尾删要说简单也简单,就是在链表的尾部插入节点嘛!先看代码:注意理解每一块的设计逻辑。
void Push_back(Link* head,ELemType x)
{
//创建一个新结点newNode ,为尾插做准备
LinkNode* newNode = (LinkNode*)malloc(sizeof(LinkNode));
assert(newNode);
newNode->date = x;
newNode->next = NULL;
//单独处理头指针
if (*head == NULL)
*head = newNode;
else
{
//定义一个cur指针,保持*head指针不动,让cur指针动,用来迭代后移,尾插newNode节点
LinkNode* cur = *head;
while (cur->next)
{
cur = cur->next;
}
//尾插
cur->next = newNode;
}
先梳理以下尾插的步骤:
- 先开辟一个新节点newNode,并为其赋值。
- 然后单独出来头结点。
- 定义cur指针,保存*head不动,移动cur,尾插newNode节点。
我们思考一个问题:假如我们不单独处理头指针*head会怎么样?
答:程序会奔溃中断。因为,我们刚开始不是已经初始化了head = NULL了嘛。假如你没有单独处理头指针head,就会发生这种:cur 指向 *head 的同时也是指向 NULL,而 cur = cur->next;语句就会变成 cur = NULL->next;哎,出问题就在这里了。用一个空指针去找next这不是明摆着“诬陷”人家NULL嘛,人家NULL里面什么都没有,你还去找next,肯定找不到啊,肯定会出事,所以单独处理很必要。
不单独处理*head的测试:
正常条件的测试:
2.头插
头插比较简单,记住要修改头指针,使它始终指向第一个节点。
void Push_front(Link* head,ELemType x)
{ //申请节点
LinkNode* newNode = (LinkNode*)malloc(sizeof(LinkNode));
assert(newNode);
newNode->date = x;
newNode->next = NULL;
//单独处理头结点
if (*head == NULL)
*head = newNode;
else
{ //头插
newNode->next = *head;
修改头指针,保证头指针指向第一个节点
*head = newNode;
}
}
测试图:
3.尾删
//尾删,最主要是找到尾节点,可是单链表不可以随机访问,也就是不能通过下标直接找到尾节点,所以要迭代去找。注意:删除节点时候要判断表是否为空,并且为了提高效率,可以单独处理只有一个节点的链表,这样就不用去迭代找了,直接删除即可。
void Pop_back(Link*phead)
{ //空链表不删
if (*head == NULL)
{
return;
}
//只有一个节点,即为头结点,也是尾节点,直接删除
else if ((*head)->next == NULL)
{
free(*head);
*head = NULL;
}
//找尾节点
else
{
LinkNode* previous = NULL; //定义一个previous指针,为了保存为节点之前的节点。
LinktNode* tail = *head;
//迭代找尾节点
while (tail->next != NULL)
{
previous = tail;
tail = tail->next;
}
free(tail);
//释放尾节点后,previous成为新的尾节点
previous->next = NULL;
}
}
4.头删
把头节点删除,记得头指针要重新指向即可。
void Pop_front(Link* head)
{
if (*head == NULL)
return;
else
{
LinkNode* newNext = (*head)->next;
free(*head);
*head = newNext;
}
}
5.申请节点
由于插入的操作都用到申请节点,其实可以直接写个函数就行。
LinkNode* BuyListNode(ElemType x)
{
SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
assert(newNode);
newNode->date = x;
newNode->next = NULL;
return newNode;
}
总结
我给出的单链表操作不多,其实这几个理解掌握就很好了,方便你以后刷leetcode那些题,当然只会这些还是不行的。刷leetcode还是需要一些技巧,这只是最基础的东西。
同时你也发现,这些操作总是要单独处理头结点,那有没有一种好的办法可以把头节点的处理和普通处理方式同一呢?其实有的,在下一篇我会讲到,就是设置一个虚拟节点就行。