目录
一、类型
1、单链表、双向链表
2、不带头单链表、带头链表
3、单链表、循环链表
二、单向+ 不带头+ 不循环链表
链表接头
函数定义:
1、打印链表
2、新节点的创建
3、尾插节点
? ? ?3.1、非空情况
? ? ?3.2、空链表情况
4、头插节点
5、尾删(三种情况)
5.1、没有节点
5.2、一个节点
5.3、多个节点
6、头删
6.1、多个节点
6.2、单个节点
7、查找节点
7、插入pos节点的后方
8、插入pos的前方
8.1、pos是链表第一个节点
8.2、pos是链表中间节点
9、删除pos节点后的节点
三、双向+ 带头+ 循环链表
链表接头
函数定义:
1.打印单链表
2、新节点的创建
2.1、新节点的初始化
3、尾插节点
? ? ?3.1、非空链表情况
4、头插节点
5、尾删
5.1、没有节点
5.2、一个节点、多个节点
6、头删
6.1、多个节点、单个节点
?编辑
7、查找节点
7、插入pos节点
8、删除pos节点后的节点
9、检查链表(空返回1,非空返回0)
10、计算链表有效值个数
11、清空链表
12、双向链表总结
顺序表和链表的优缺点对比
一、类型
1、单链表、双向链表
2、不带头单链表、带头链表
3、单链表、循环链表
二、单向+ 不带头+ 不循环链表
链表的定义结构
typedef int SLTDataType;//方便后期修改数据类型
typedef struct SListNode
{
SLTDataType data; //数据域
struct SListNode* next;//指针域
}SLTNode;
接头函数声明
//单项+ 不带头+ 不循环
void SListPrint(SLTNode* plist);
void SListPushBack(SLTNode** pplist, SLTDataType x);//尾插
void SListPushFront(SLTNode** plist, SLTDataType x);//头插
void SListPopBack(SLTNode** pplist);//尾删
void SListPopFront(SLTNode** pplist);//头删
SLTNode* SListFind(SLTNode* plist, SLTDataType x);//搜索
void SListInsertAfter(SLTNode* pos, SLTDataType x);//任意插入
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x);
//任意插入pos位置前
void SListEraseAfter(SLTNode* pos);//删除pos后节点
//void SListEraseCur(SLTNode* pos);//删除pos
函数定义:
?1.打印链表
void SListPrint(SLTNode* plist)
{
???????SLTNode* cur = plist;
???????while (cur != NULL)
???????{
??????????????printf("%d ", cur->data);
??????????????cur = cur->next;
???????}
???????printf("\n");
}
2、新节点的创建
//将所需数据放入节点里,return 创建的地址,需要一个指针接收
SLTNode* BuySLTNode(SLTDataType x)
{
???????SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
???????node->data = x;
???????node->next = NULL;
???????return node;
}
3、尾插节点
? ? ?3.1、非空情况
? ? ?3.2、空链表情况
void SListPushBack(SLTNode** pplist, SLTDataType x)//尾插
{
???????//创建节点存储
???????SLTNode* newnode = BuySLTNode(x);
???????//1、空的情况
???????//2、非空情况
???????if (*pplist == NULL)
???????{
??????????????*pplist = newnode;//修改的是地址,且函数无return,所以用二级指针
???????}
???????else
???????{
??????????????//找尾
??????????????SLTNode* tail = *pplist;
??????????????while (tail->next != NULL)
??????????????{
??????????????????????tail = tail->next;
??????????????}
??????????????tail->next = newnode;//用节点尾巴去接上新节点
???????}
}
4、头插节点
void SListPushFront(SLTNode** pplist, SLTDataType x)//头插
{
???????SLTNode* newnode = BuySLTNode(x);
???????newnode->next = *pplist; //先将new的next指向pplist原本指向的节点
???????*pplist = newnode;???????//再将new节点的地址设置为pplist
}
5、尾删(三种情况)
5.1、没有节点
? ? -直接return,或者断言
5.2、一个节点
将其free再置空
5.3、多个节点
void SListPopBack(SLTNode** pplist)
{
//1、没有节点
//2、一个节点
//3、多个节点
if (*pplist == NULL)
{
return;
}
else if((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SLTNode* prev = NULL;//前驱指针
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
prev = tail; //前驱后移
tail = tail->next; //tail也后移
}
free(tail);//将tail置空,此时plist的最后一个节点也被置空
tail = NULL;//防止野指针
prev->next = NULL;//将尾指针置空
}
}
6、头删
6.1、多个节点
6.2、单个节点
void SListPopFront(SLTNode** pplist)
{
if (*pplist == NULL)
{
return;
}
else
{
SLTNode* next = (*pplist)->next;//制定一个next存放首节点后的节点
free(*pplist);
*pplist = next;
}
}
7、查找节点
//查找,返回节点x的指针
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
SLTNode* cur = plist;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
7、插入pos节点的后方
void SListInsertAfter(SLTNode* pos, SLTDataType x)//任意插入
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;//先将新封装的节点连接到d3
pos->next = newnode;//再讲d2的next连接到新节点
}
8、插入pos的前方
8.1、pos是链表第一个节点
8.2、pos是链表中间节点
//任意插入pos位置前
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
//只要涉及到改变第一个节点,也就是要修改传入的指针变量
//那么就要用二级指针
assert(pos);
SLTNode* newnode = BuySLTNode(x);
if (pos == *pplist)//就是头插
{
newnode->next = pos;
*pplist = newnode;//将新节点指定为第一个节点,用二级指针传值
}
else
{
SLTNode* prev = NULL;
SLTNode* cur = *pplist;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
思考:
在一个无头单链表的某一节点的前面插入一个值x,如何插入?(不告诉头指针)
- 后插入一个值为x的节点,然后和前面的节点值data交换
9、删除pos节点后的节点
对于两种情况,一种解决方案即可
void SListEraseAfter(SLTNode* pos)//删除节点
{
assert(pos);
if (pos->next == NULL)
{
return;
}
else
{
SLTNode* next = pos->next;//用next存放d3
pos->next = next->next;//再将pos连接到d3的后一节点
free(next);//释放d3
}
}
10、删除当前pos
思路:
三、双向+ 带头+ 循环链表
链表的定义结构
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;//前驱指针
LTDataType data;
}ListNode;
接头函数声明
ListNode* BuyListNode(LTDataType x);//创建节点
ListNode* ListInit();//初始化节点
void ListPrint(ListNode* phead);//打印链表
void ListPushBack(ListNode* phead, LTDataType x);//尾插
void ListPushFront(ListNode* phead, LTDataType x);
void ListPopBack(ListNode* phead);
void ListPopFront(ListNode* phead);
ListNode* ListFind(ListNode* phead, LTDataType x);
void ListInsert(ListNode* pos, LTDataType x);
void ListErase(ListNode* pos);
//空返回1,非空返回0
int ListEmpty(ListNode* phead);
int ListSize(ListNode* phead);
void ListDestory(ListNode* phead);
函数定义:
1.打印单链表
void ListPrint(ListNode* phead)
{
ListNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
2、新节点的创建
//将所需数据放入节点里,return 创建的地址,需要一个指针接收
ListNode* BuyListNode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
2.1、新节点的初始化
- 双向带头循环链表在初始化只有一个头节点时,要见头尾指向自己
ListNode* ListInit()
{
ListNode* phead = BuyListNode(0);
phead->next = phead;//让头指向指向自己
phead->prev = phead;//让尾指向指向自己
return phead;
}
3、尾插节点
? ? ?3.1、非空链表情况
在只有一个头节点或者多个节点都可以一次实现
void ListPushBack(ListNode* phead, LTDataType x)
{
assert(phead);
//初链表只有一个节点或者多节点都可以实现
ListNode* tail = phead->prev;//因为循环,所以头节点的前一个是为节点,用tail记录
ListNode* newnode = BuyListNode(x);
tail->next = newnode;//将原尾节点tail连接新节点
newnode->prev = tail;//新节点的前驱节点设置为tail
newnode->next = phead;//新节点的下一节点设置为头节点
phead->prev = newnode;//头节点的前驱指针指向新节点
//此方法也是尾插
/*ListInsert(phead, x);*/
}
4、头插节点
void ListPushFront(ListNode* phead, LTDataType x)
{
assert(phead);
//头插注意要先将newnode先连接第一有数据节点(不是头指针)
//或者设置一个新的指针(frist)先存放第一有数据节点
ListNode* first = phead->next;
ListNode* newnode = BuyListNode(x);
//开始连接三个指针
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
//此方法也是头插
/*ListInsert(phead->next, x);*/
}
5、尾删
5.1、没有节点
? ? 删除要注意断言只有头指针的情况
5.2、一个节点、多个节点
两种情况一种解决方案
void ListPopBack(ListNode* phead)
{
//不用遍历即可找到尾项
assert(phead);
assert(phead->next != phead);//如果链表只有头节点在此情况也是错误链表
ListNode* tail = phead->prev;//设置指针指向原尾项
ListNode* tailPrev = tail->prev;//设置指针指向倒数第二项
free(tail);//释放最后一项
tailPrev->next = phead;//倒数第二项晋升为最后一项,用其连接头节点
phead->prev = tailPrev;//将头节点的前驱指针指向新尾节点
//此方法也是尾插
/*ListErase(phead->prev);*/
}
6、头删
6.1、多个节点、单个节点
两种情况一种解决方案
void ListPopFront(ListNode* phead)
{
assert(phead);
assert(phead->next != phead);//如果链表只有头节点也是错误链表
ListNode* frist = phead->next;
ListNode* fristNext = frist->next;
free(frist);
fristNext->prev = phead;
phead->next = fristNext;
//此方法也是头插
/*ListErase(phead->next);*/
}
7、查找节点
ListNode* ListFind(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;//没找到返回空
}
7、插入pos节点
- 插入前(对于双向链表在pos前后插入节点都很简单)
- 就算pos是头节点 或者 是最后一个节点 都很容易实现
- 不过pos是头节点用此方法相当于 尾插
- pos是phead->next则相当于 头插
//插入前(对于双向链表在pos前后插入节点都很简单)
//就算pos是头节点 或者 是最后一个节点 都很容易实现
//不过pos是头节点用此方法相当于 尾插
//pos是phead->next则相当于 头插
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* newnode = BuyListNode(x);
prev->next = newnode;//双向
newnode->prev = prev;
newnode->next = pos;//双向
pos->prev = newnode;
}
8、删除pos节点后的节点
对于两种情况,一种解决方案即可
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
9、检查链表(空返回1,非空返回0)
int ListEmpty(ListNode* phead)
{
assert(phead);
return phead->next == phead ? 1 : 0;
}
10、计算链表有效值个数
int ListSize(ListNode* phead)
{
assert(phead);
int size = 0;
ListNode* cur = phead->next;
while (cur != phead)
{
++size;
cur = cur->next;
}
return size;
}
11、清空链表
void ListDestory(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;//此处没用,需要传递二级指针,但是会破坏一致性
//最后要置空头指针,需要在.c文件最后plist = NULL;置空
}
12、双向链表总结
只要拥有7、8函数表头,即可实现头插尾插,头删尾删
但是要注意phead节点的使用
//尾插节点
void ListPushBack(ListNode* phead, LTDataType x)
{
ListInsert(phead, x);
}
//头插
void ListPushFront(ListNode* phead, LTDataType x)
{
ListInsert(phead->next, x);
}
//尾删
void ListPopBack(ListNode* phead)
{
ListErase(phead->prev);
}
//头删
void ListPopFront(ListNode* phead)
{
ListErase(phead->next);
}
顺序表和链表的优缺点对比
(双向带头循环链表)
1、按下标去进行随机访问
2、 CPU高速缓存命中率较高
1、 空间不够需要增容(一定程序的性能消耗),可能存在一定的空间浪费
2、 头部或中间插入删除数据,需要挪动数据,效率比较低->O(N)
1、 按需申请内存,需要存一个数据,就申请一块内存,不存在浪费
2、 任意位置O(1) 时间内插入删除数据
1、 不支持下标的随机访问
总结: 这两数据结构是相辅相成的,互相弥补对方的缺点,需要用谁存数据,具体看场景
|