前言:
前面说到了顺序表,顺序表的特点是存储是按顺序存储,只要能找到顺序表中的任意一个元素,其他元素就可以顺利找到。
当然顺序表也是有一定的缺陷,利用顺序表存储数据,开辟空间的大小不好掌控,一般取每次开上一次的二倍比较合理一点,但是仍有空间浪费的情况。其次在顺序表中进行头插,头删都需要挪动数据,时间复杂度为O(n);
在此引出了链表,链表可以弥补顺序表的缺陷,但是自身也是有缺陷,接下来进入链表的讲解说明。
链表:
概念:
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
链表的分类:
1、带头或者不带头2、双向或者单向
3、循环或者非循环
常见的链表:
1、无头非循环单向链表
2、带头非循环单向链表
3、无头循环单向链表
4、带头双向循环链表
无头非循环单向链表的组成:
1、本身的值,int(char,long,short,...)val;
2、结构体指针(连接下一个结构体),struct ListNode*next;
针对这两部分可以通过画图来说明一下他们的作用。
一个结构体中有这两部分组成:
多个结构体就是这样:
注意:
1、前一个结构体中需要存放下一个结构体的地址,便于查找下一个结构体。
2、最末尾的结构体中需要存放一个空指针(不能忘记)
无头非循环单向链表的实现:
注:我们还是采用三文件的方式完成链表。
定义结构体:
定义结构体需要在SList.h的头文件中完成。
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#define SLNDataType int
typedef struct SListNode
{
SLNDataType val;
struct SListNode* next;
}SListNode;
注:由于不确定结构体中存储的值的类型,这里将采用#define定义的方式,如果类型不同,可以直接通过修改SLNDataType int来进行全局的修改。
动态申请一个节点:
写一个函数SListNode* BuySListNode(SLTDateType x),每次需要申请空间,可以直接调用该函数。(在SList.c中定义函数)
#include"SList.h"
SListNode* BuySListNode(SLNDataType x)
{
SListNode* tmp = (SListNode*)malloc(sizeof(SListNode));
if (tmp == NULL)
{
perror("BuySlListNode malloc::");
exit(-1);
}
tmp->next = NULL;
tmp->val = x;
return tmp;
}
单链表的打印:
写一个函数void SListPrint(SListNode* plist)实现链表的打印。
方法:
1、需要注意一个无头循环单链表的末尾是指向NULL的指针。
2、如何让指针往后走,那就要抓住*next,也就是让head = head->next。
void SListPrint(SListNode* plist)
{
while (plist)
{
printf("%d->", plist->val);
plist = plist->next;
}
printf("NULL");
printf("\n");
}
单链表的尾插:
写一个函数void SListPushBack(SListNode** pplist, SLNDataType x),可以实现单链表的尾插。
注意:
在找尾的时候需要注意不要用原来的指针找,要让头指针保持不动!
void SListPushBack(SListNode** pplist, SLNDataType x)
{
SListNode* cur = BuySListNode(x);
SListNode* tmp = *pplist;
if (*pplist == NULL)
{
*pplist = cur;
}
else
{
//找尾
while (tmp->next)
{
tmp = tmp->next;
}
tmp->next = cur;
}
}
大家可以先想一想,
这里为什么要用二级指针?
而且为什么要用 if 分情况讨论?
解答第一个问题我们可以从主函数出发:
要实现尾插,有两种传参方式,我们可以看看
第一种:定义一个函数体指针,然后将函数体指针的值直接传进去。
void test1()
{
SListNode* head = NULL;
SListPushBack(head,3);
}
第二种:定义一个函数体指针,然后将函数体指针的地址传进进去。
void test1()
{
SListNode* head = NULL;
SListPushBack(&head,3);
}
这两种有什么区别:
第一种:
此时如果我改phead的指向,head的指向是不会改变的,此时phead里面的地址,只不过是head的一份临时拷贝。
第二种:
此时如果我首先解引用*phead,就相当于找到了head,然后再进行指向的修改,就相当于修改head的指向。
当然,如果有人问,我就是搞不懂,也不行传二级指针,这时候就需要接触到带头非循环单链表。
也就是带哨兵位的,大概样子就是这样。
相当于第一个头里面不存有效值,只存储第二个结构体的地址。也就是说头节点本身的地址不可能为NULL,他一定会存放第二个结点的地址,也就是不论如何头节点一定在。
这时候,如果我传参的时候,传入的就是头结点的地址,这个问题就可以用一级指针解决:
代码如下:
void test1()
{
SListNode* head = (SListNode*)malloc(sizeof(SListNode));//开辟头节点
head->next = NULL;
SListPushBack(head,3);
}
当然如果选择这样去做,那么所有的函数需要改变一下,也就是第一个有效节点就会变为
phead->next !!!
单链表的头插:
写一个函数void SListPushFront(SListNode** pplist, SLTDateType x),实现单链表的头插。
需要注意也有两种情况:
第一种情况:
第二种情况:
void SListPushFront(SListNode** pplist, SLNDataType x)
{
SListNode* cur = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = cur;
}
else
{
SListNode* tmp = (*pplist)->next;
*pplist = cur;
cur->next = tmp;
}
}
单链表的尾删:
写一个函数void SListPopBack(SListNode** pplist),实现链表的尾删。
注意:
1、如果是一个空链表就不需要删除!2、需要找到尾的前一个
void SListPopBack(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
SListNode* cur = *pplist;
//找尾的前一个
while (cur->next->next)
{
cur = cur->next;
}
free(cur->next);
cur->next = NULL;
}
单链表头删
写一个函数void SListPopFront(SListNode** pplist),实现单链表的头删。
注意:
在free之前一定要先保存一份(*pplist)->next。
void SListPopFront(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
SListNode* tmp = (*pplist)->next;
free(*pplist);
(*pplist) = tmp;
}
单链表查找
写一个函数SListNode* SListFind(SListNode* plist, SLTDateType x),实现单链表的查找。
SListNode* SListFind(SListNode* plist, SLNDataType x)
{
assert(plist);
while (plist)
{
if (plist->val == x)
{
return plist;
}
plist = plist->next;
}
return NULL;
}
单链表的任意位置插入:
写函数void SListInsertAfter(SListNode* pos, SLTDateType x),实现在任意位置插入。
注:
1、SListInsertAfter函数只能在选定位置的后面插入。
2、SListInsertAfter函数需要配合链表的查找功能一起使用
void SListInsertAfter(SListNode* pos, SLNDataType x)
{
assert(pos);
SListNode* tmp = pos->next;
SListNode* cur = BuySListNode(x);
pos->next = cur;
cur->next = tmp;
}
写作实属不易,能否点赞再走
如有哪里理解有误,希望大家多多指点!!