目录
一、类型
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、双向链表总结
顺序表和链表的优缺点对比
链表的定义结构
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
void SListPrint(SLTNode* plist)
{
SLTNode* cur = plist;
while (cur != NULL)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
//将所需数据放入节点里,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,如何插入?(不告诉头指针)
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);
void ListPrint(ListNode* phead)
{
ListNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
//将所需数据放入节点里,return 创建的地址,需要一个指针接收
ListNode* BuyListNode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
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;置空 }
只要拥有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、 不支持下标的随机访问
总结: 这两数据结构是相辅相成的,互相弥补对方的缺点,需要用谁存数据,具体看场景