链表是一种常见的数据结构,用于线性方式存储数据,链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表是一个一个内存块,用指针把它们链接起来。
链表适用于当数据结合频繁变化,需要快速插入和删除,内存空间分散的场景。
上 一个节点存下一个节点的地址,每一个节点的类型都是一个结构体,这里每个结构体里面不是结构体,是个指针,这个指针的类型是struct SListNode。
简单定义一下
//为了后续能方便替换成其他类型,如果直接在结构体里用int可能写的多了会混淆,也是好分辨一点
typedef int SLTDataType;
struct SListNode
{
SLTDataType data;
struct SListNode* next;
};
//因为这里每次用到结构体类型struct SListNode比较长所以用typedef把它定义短一点
//可以单独写一行定义
typedef struct SListNode SLTNode;
//也可以直接在定义链表时用typedef,这两种写法是等价的
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
动态内存分配:链表中的节点是在运行时动态创建的,可以根据需要分配和释放内存。
可扩展性:链表的大小可以根据需要动态增长或缩小,没有固定的大小限制。
非连续存储:链表中的节点可以分散在内存的不同位置,每个节点包含数据和指向下一个节点的指针。
1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
⒉.带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
常见的链表就有不带头单向非循环链表和带头双向循环链表。
单链表(不带头单向非循环链表)
插入:在链表的特定位置添加新节点。
删除:移除链表中的特定节点。
搜索:查找链表中的特定值。
遍历:按顺序访问链表的所有节点。
定义
//假设这里为头文件,这里放结构体和函数的声明
//SList.h
#pragam once
#includ <stdio.h>
#include
#include
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
void SLTPrint(SLTNode* phead); //链表的打印
//因为涉及到phead指针的修改,所以参数得传二级指针
void SLTPushBack(SLTNode** pphead, SLTDataType x); //链表的尾插
void SLTPushFront(SLTNode** pphead ,SLTDataType x); //链表的头插
void SLTPophBack(SLTNode** pphead); //链表的尾删
void SLTPopFront(SLTNode** pphead); //链表的头删
//单链表查找,通常也可以用来简单修改一下值
SLTNode* SLTFind(SLTNode*phead, SLTDataType x);
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x); //在指定位置之前插入数据
void SLTInsertAfter(SLTNode** pphead,SLTNode* pos,SLTDataType x); //在指定位置之后插入数据
void SLTErase(SLTNode** pphead,SLTNode* pos); //删除pos节点
void SLTEraseAfter(SLTNode** pphead,SLTNode* pos); //删除pos之后节点
void SLTDestroy(SLTNode** pphead); //销毁链表
#include "SList.h"
//创建新节点,需要新节点时直接调用该函数就行了
SLTNode* BuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTDataType*)malloc(sizeof(SLTDataType));
if(newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while(cur)
{
printf("%d->",cur->data);
cur=cur->next;
}
printf("NULL\n");
}
//原尾节点中要存储新的尾节点的地址
void SLTPushBack(STLNode** pphead,SLTDataType x)
{
asssert(pphead); //pphead也就是*pphead的地址,pphead不能为空,如果pphead为空了那么*pphead地址在哪
SLTNode* newnode = BuyNode(x);
if(tail->next == NULL) //空链表
{
*pphead = newnode;
}
else //链表不为空
{
SLTNode* tail = NULL;
while(tail->next != NULL)
{
tail = tail->next;
}
tail->next = x;
}
}
//把新节点链接在链表头部
void SLTPushFront(SLTNode** pphead,SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuyNode(x);
newnode->next = *pphead;
*pphead->next = newnode;
}
//要注意判断只有一个节点的情况和判断链表为空的情况
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead); //空链表
if((*pphead)->next == NULL) //只有一个节点
{
free(*pphead);
*pphead = NULL;
}
else //多个节点
{
SLTNode* tail = *pphead;
while(tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
/*或在定义一个变量,两种方法都可以
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while(tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev = NULL;
*/
}
}
//删除头节点前要把下一个节点存储起来
void SLTPopFront(SLTNode** pphead)
{
assert(*pphead);
SLTNode* first = *pphead;
*pphead = first->next;
free(first);
first = NULL;
}
//遍历链表
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
//空节点返回null,不用assert
SLTNode* cur = phead;
while(cur)
{
if(cur->data == x) //找cur成员里有没有x
{
return cur;
}
cur = cur->next;
}
return NULL; //如果以上都没有返回值,则找不到,返回空指针
}
//1.注意判断空链表;不能在空节点前插入(尾插)。2.pos不能为空。3.pos是头节点的情况。
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x)
{
assert(pos); //pos不能为空
//pos是头节点的情况,直接调用头插函数
if(pos == *pphead)
{
SLTPushFront(pphead,x);
}
else
{
SLTNode* prev = *pphead;
while(prve->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuyNode(x);
newnode->next = pos;
prev->next = newnode;
}
}
//删除pos节点
void SLTErase(SLTNode** pphead,SLTNode* pos)
{
assert(pphead);
assert(pos);
if(*pphead == pos) //如果pos是头指针
{
SLTPopFront(pphead,pos); //调用头删函数
}
else
{
SLTNode* prev = *pphead;
while(prev->next != pos) //找到pos的前一个位置
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
//在pos后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuyNode(x);
//下面两行顺序不能乱
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos后的数据,注意判断pos之后的节点是不是空节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* del = pos->next;
pos->next = pos->next->next;
free(del);
del = NULL;
}
//销毁链表
void SLTDestroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = pcur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
测试函数
#include "SList.h"
void test1()
{
SLTNode* plist = NULL;
SLTPushBack(&plist,1);
SLTPushBack(&plist,2);
SLTPushBack(&plist,3);
SLTPushBack(&plist,4);
SLTPrint(plist); //打印:1->2->3->4->NULL
SLTPopBack(&plist);
SLTPrint(plist); //打印:1->2->3->NULL
}
void test2()
{
SLTNode* plist;
SLTPushFront(&plist,1);
SLTPushFront(&plist,2);
SLTPushFront(&plist,3);
SLTPushFront(&plist,4);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPopFront(&plist);
SLTPrint(plist); //打印:2->1->NULL
}
void test3()
{
SLTNode* plist=NULL;
SLTPushBack(&plist,1);
SLTPushBack(&plist,2);
SLTPushBack(&plist,3);
SLTPushBack(&plist,4);
SLTPrint(plist);
//把值为2的节点乘2
SLTNode* ret = SLTFind(plist,3);
ret->data *= 2;
SLTPrint(plist); //打印:1->2->6->4->NULL
SLTInsert(&plist,ret,66); //ret的位置是查找函数x的值
SLTPrint(plist); //打印:1->2->66->6->4->NULL
//后面也类似就不演示了
}
int main()
{
test1();
test2();
test3();
return 0;
}
循环的特点:
1.尾next指向哨兵位的头
2.哨兵位的头prev指向尾
有了单链表的基础,这里我们就直接定义了
//List.h
#pragma once
#include
#include
#include
#include
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType data;
}LTNode;
void LTInit(LTNode* phead);
void LTDestroy(LTNode* phead);
void LTPrint(LTNode* phead);
bool LTEmpty(LTNode* phead); //判断链表为空
//因为这里是直接改的指针变量所以不用二级
void LTPushBack(LTNode* phead,LTDataType x);
void LTPushFront(LTNode* phead,LTDataType x);
void LTPopBack(LTNode* phead);
void LTPopFront(LTNode* phead);
void LTInsert(LTNode* pos,LTDataType x);
void LTErase(LTNode* phead);
void LTDestroy(LTNode* phead); //遍历链表,逐个销毁,然后销毁哨兵位就行了
实现
#include "List.h"
LTNode* BuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if(newnode == NULL)
{
perror("malloc fail");
return NULL;
//exit(-1); 或使用这个终止程序
}
newnode->prev = NULL;
newnode->next = NULL;
newnode->data = x;
return newnode;
}
bool Empty(LTNode* phead)
{
assert(phead);
return phead == phead->next; //如果phead->next等于phead那就是空,相等就是真,真就是空
}
void LTInit(LTNode* phead)
{
LTNode* Phead = BuyNode(-1);
phead->prev = phead->next = phead; //双向链表是带头双向循环的,所以初始情况哨兵节点前指针和后指针要指向自己,不然不是循环
return phead;
}
void LTPrint(LTNode* phead)
{
printf("<=>");
LTNode* cur = phead;
while(cur)
{
printf("%d<=>",cur->data);
cur = cur->next;
}
printf("\n");
}
//双向链表是空链表时,指的是只有一个哨兵位
//保证链表不是空链表
void LTPushBack(LTNode* phead,LTDataType x)
{
assert(phead);
LTNode* newnode = BUyNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
void LTPushFront(LTNode* phead,LTDataType x)
{
LTNode* newnode = BuyNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
void LTPopBack(LTNode* phead);
{
assert(phead);
assert(!Empty(phead)); //断言它不等于空
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
tailPrev->next = phead;
phead->prev = tailPrev;
free(tail);
tail = NULL;
}
void LTPopFront(LTNode* phead)
{
assert(phead);
LTErase(phead->next);
}
void LTInsert(LTNode* pos,LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
#include "List.h"
void test1()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist); //打印:<=phead=>1<=>2<=>3<=>4<=>
LTPopBack(plist);
LTPrint(plist); //打印:<=phead=>1<=>2<=>3<=>
}
void test2()
{
LTNode* plist = LTInit();
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
LTPushFront(plist, 4);
LTPrint(plist);
}
void test3()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist); //打印:<=phead=>1<=>2<=>3<=>4<=>
LTNode* pos = LTFind(plist, 2); //找到链表里值为2的
if (pos) //如果链表中有该值
{
LTErase(pos); //删除2
pos = NULL;
}
LTPrint(plist); //打印:<=phead=>1<=>3<=>4<=>
LTPopFront(plist);
LTPrint(plist); //打印:<=phead=>3<=>4<=>
LTDestroy(plist);
plist = NULL; //在调用函数的这里置空指针
}
int main()
{
test1();
test2();
test3();
return 0;
}
因为单链表是不带头不循环,链表最开始没有插入数据链表为空初始状态,直接赋值为NULL就好了没必要单独写个函数 。
顺序表单独写个初始化函数是因为:顺序表为空的标志是size=0;capacity看需不需要开空间,不开空间等于0,指针为NULL;开空间的话还要malloc检查,有很多事的就交给函数去做。
在一定不能为空的情况下使用assert ,参数传错,断言是进行一些比较明显典型错误的前期检查。
为什么会有pphead?
如果头插头删等这些需要动指针光传plist不可以,得传plist的地址,只要传plist的地址就有pphead,所以只要用pphead的地方都要进行断言防止传错,例如一级传给二级指针。
1:链表的主体是一个一个的节点每个节点用指针链接起来,访问链表要用指针找链表的内容。
2:单链表和双向链表的插入函数和删除函数也可以用来头插、头删、尾插、尾删。
3:顺序表是在一个数组里,在数组里我们要知道容量多大,实际数据个数多少,所以用了一个结构体,结构体里有个指针指向顺序表,但是它除了指针还需要有size和capacity,size用来标识有多少数据,capacity用来进行扩容。
本质都是指针,需要加size知道有多少数据,需要加capacity方便扩容,而链表不需要size遇到NULL就结束了只需要有一个指针指向第一个节点。