目录
1. 链表的概念及优缺点
1.1 链表的概念
链表是一种物理存储结构上非连续、非顺序的存储结构,
数组元素的逻辑顺序是通过链表中的指针链接次序实现的。
![](https://i-blog.csdnimg.cn/blog_migrate/0abe52afba5b81bc091475d0626744ac.png)
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向、双向
2. 带头、不带头
3. 循环、非循环
![](https://i-blog.csdnimg.cn/blog_migrate/1fb5bdff0eabd6aef658eb3cb40b22de.png)
1. 无头单向非循环链表:(下面实现的就是这种结构)
结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,
如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,
都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
1.2 顺序表和链表的优缺点
为什么顺序表和链表各自存在?
因为他们各有优点,他们是互补的,并不是有他没我,有我没他的。
顺序表的优点:
![](https://i-blog.csdnimg.cn/blog_migrate/eade9191fc34805511613e5f371e36c7.png)
① 支持随机访问,有些算法需要结构随机访问,比如二分查找和优化的快排等。
② 数据是按顺序存放的,空间利用率高。
③ 通过下标直接访问,存取速度高效。
顺序表的缺点:
① 中间/头部的插入删除,需要挪动,挪动数据时也是存在消耗的,时间复杂度为O(N)
② 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
③ 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,
我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
链表的优点:
① 按需申请空间,不用就释放空间( 链表可以更合理地使用空间)。
② 头部或者中间位置的插入和删除,不需要挪动数据。
③ 不存在空间浪费。
链表的缺陷:
① 每一个数据,都要存一个指针去链接后面的数据节点。
② 不支持随机访问(用下标直接访问第 i 个),必须得走 O(N) 。
单链表的缺陷还是很多的,单纯单链表的增删查找的意义不大,
但是很多OJ题考察的都是单链表。单链表多用于更复杂的数据结构的子结构,
如哈希桶,邻接表等。所以真正存储数据还是得靠双向链表。
2. 链表的实现(完整代码)
还是和上一篇顺序表的实现一样,主要实现增删查改功能,可以一个一个函数看,很简单。
Slist.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLNDataType;
typedef struct SlistNode //single单一的 node节点
{
SLNDataType data;
struct SlistNode* next;
}SLNode;
void SListPrint(SLNode* phead);
void SListPushBack(SLNode** pphead, SLNDataType x);
void SListPushFront(SLNode** pphead, SLNDataType x);
SLNode* CreateNewNode(SLNDataType x);
void SListPopBack(SLNode** pphead);
void SListPopFront(SLNode** pphead);
SLNode* SListFind(SLNode* phead, SLNDataType x);
void SListInsert(SLNode** pphead, SLNode* pos, SLNDataType x);
void SListInsertAfter(SLNode* pos, SLNDataType x);
void SListEarse(SLNode** pphead, SLNode* pos);
void SListEarseAfter(SLNode* pos);
void SListDestroy(SLNode** pphead);
SList.c
#include"SList.h"
void SListPrint(SLNode* phead)
{
SLNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
void SListPushBack(SLNode** pphead, SLNDataType x)
{
SLNode* NewNode = CreateNewNode(x);
if (*pphead == NULL)
{
*pphead = NewNode;
}
else
{
SLNode* tail = *pphead;//tail 尾巴
while (tail->next != NULL)//找到尾节点
{
tail = tail->next;
}
tail->next = NewNode;
}
}
SLNode* CreateNewNode(SLNDataType x)
{
SLNode* NewNode = (SLNode*)malloc(sizeof(SLNode));
if (NewNode == NULL)
{
printf("malloc failed!\n");
exit(-1);
}
NewNode->data = x;
NewNode->next = NULL;
return NewNode;
}
void SListPushFront(SLNode** pphead, SLNDataType x)
{
SLNode* NewNode = CreateNewNode(x);
NewNode->next = *pphead;
*pphead = NewNode;
}
void SListPopBack(SLNode** pphead)
{
assert(*pphead != NULL);
if ((*pphead)->next == NULL)//如果只有一个节点
{
free(*pphead);
*pphead = NULL;
}
else
{
SLNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
void SListPopFront(SLNode** pphead)
{
assert(*pphead != NULL);
SLNode* begin = *pphead;
*pphead = (*pphead)->next;
free(begin);
begin = NULL;
}
SLNode* SListFind(SLNode* phead, SLNDataType x)
{
SLNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
void SListInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
SLNode* NewNode = CreateNewNode(x);
if (*pphead == pos)
{
NewNode->next = *pphead;
*pphead = NewNode;
}
else
{
SLNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = NewNode;
NewNode->next = pos;
}
}
void SListInsertAfter(SLNode* pos, SLNDataType x)
{
assert(pos != NULL);
SLNode* NewNode = CreateNewNode(x);
NewNode->next = pos->next;
pos->next = NewNode;
//和前面对比,单链表的缺陷是要找pos的前一个要从头往后找
}
void SListEarse(SLNode** pphead, SLNode* pos)
{
assert(*pphead != NULL);
assert(pos != NULL);
if (*pphead == pos)//如果是头删
{
*pphead = pos->next;
free(pos);
pos = NULL;
}
else
{
SLNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = pos->next;
free(pos);
pos = NULL;//可以不写,形参不会改变实参,这个野指针会被栈帧销毁,但是个好习惯emm
}
}
void SListEarseAfter(SLNode* pos)
{
//1 2 3 4
assert(pos != NULL);
assert(pos->next != NULL);
SLNode* posAfter= pos->next;
pos->next = posAfter->next;
free(posAfter);
posAfter = NULL;//可以不写,形参不会改变实参,这个野指针会被栈帧销毁
}
void SListDestroy(SLNode** pphead)
{
assert(pphead);
SLNode* cur = *pphead;
while (cur)
{
SLNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
Test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void TestSList1()//测试尾插头插
{
SLNode* pList = NULL;
SListPushBack(&pList, 1);
SListPushBack(&pList, 2);
SListPushBack(&pList, 3);
SListPushBack(&pList, 4);
SListPrint(pList);
SListPushFront(&pList, 1);
SListPushFront(&pList, 2);
SListPushFront(&pList, 3);
SListPushFront(&pList, 4);
SListPrint(pList);
}
void TestSList2()//测试尾删头删
{
SLNode* pList = NULL;
SListPushBack(&pList, 1);
SListPushBack(&pList, 2);
SListPushBack(&pList, 3);
SListPushBack(&pList, 4);
SListPrint(pList);
SListPopBack(&pList);
SListPrint(pList);
SListPopFront(&pList);
SListPrint(pList);
SListPopBack(&pList);
SListPopBack(&pList);
SListPrint(pList);
}
void TestSList3()//测试查找函数
{
SLNode* pList = NULL;
SListPushFront(&pList, 1);
SListPushFront(&pList, 2);
SListPushFront(&pList, 3);
SListPushFront(&pList, 2);
SListPushFront(&pList, 4);
SListPushFront(&pList, 2);
SListPushFront(&pList, 2);
SListPushFront(&pList, 4);
SListPrint(pList);
SLNode* pos = SListFind(pList, 2);
for (int i = 0;pos != NULL;i++)
{
printf("第%d个pos节点:%p->%d\n", i, pos, pos->data);
pos->data = 9;//查找过程中把2改成9 返回SLNode*的好处
pos = SListFind(pos->next, 2);
}
SListPrint(pList);
}
void TestSList4()//测试指定位置的前面插入
{
SLNode* pList = NULL;
SListPushBack(&pList, 1);
SListPushBack(&pList, 2);
SListPushBack(&pList, 3);
SListPushBack(&pList, 4);
SListPrint(pList);
SLNode* pos = SListFind(pList, 2);
SListInsert(&pList, pos, 60);
SListPrint(pList);
pos = SListFind(pList, 1);
SListInsert(&pList, pos, 70);
SListPrint(pList);
SListInsert(&pList, NULL, 80);
SListPrint(pList);
}
void TestSList5()//测试指定位置的后面插入
{
SLNode* pList = NULL;
SListPushBack(&pList, 1);
SListPushBack(&pList, 2);
SListPushBack(&pList, 3);
SListPushBack(&pList, 4);
SListPrint(pList);
SLNode* pos = SListFind(pList, 3);
SListInsertAfter(pos, 10);
SListPrint(pList);
pos = SListFind(pList, 4);
SListInsertAfter(pos, 20);
SListPrint(pList);
pos = SListFind(pList, 1);
SListInsertAfter(pos, 30);
SListPrint(pList);
}
void TestSList6()//测试指定位置的删除
{
SLNode* pList = NULL;
SListPushBack(&pList, 1);
SListPushBack(&pList, 2);
SListPushBack(&pList, 3);
SListPushBack(&pList, 4);
SListPrint(pList);
SLNode* pos = SListFind(pList, 3);
SListEarse(&pList, pos);
SListPrint(pList);
pos = SListFind(pList, 1);
SListEarse(&pList, pos);
SListPrint(pList);
pos = SListFind(pList, 2);
SListEarse(&pList, pos);
SListPrint(pList);
pos = SListFind(pList, 4);
SListEarse(&pList, pos);
SListPrint(pList);
}
void TestSList7()//测试指定位置之后的删除和整个链表销毁
{
SLNode* pList = NULL;
SListPushBack(&pList, 1);
SListPushBack(&pList, 2);
SListPushBack(&pList, 3);
SListPushBack(&pList, 4);
SListPrint(pList);
//SListDestroy(&pList);
//SListPrint(pList);
SLNode* pos = SListFind(pList, 3);
SListEarseAfter(pos);
SListPrint(pList);
pos = SListFind(pList, 1);
SListEarseAfter(pos);
SListPrint(pList);
pos = SListFind(pList, 1);
SListEarseAfter(pos);
SListPrint(pList);
SListDestroy(&pList);
SListPrint(pList);
}
int main()
{
//TestSList1();//测试尾插头插
//TestSList2();//测试尾删头删
//TestSList3();//测试查找函数
//TestSList4();//测试指定位置的前面插入
//TestSList5();//测试指定位置的后面插入
//TestSList6();//测试指定位置的删除
TestSList7();//测试指定位置之后的删除和整个链表销毁
return 0;
}
3. 笔试选择题练习
练习1
下列关于链表的说法哪个是正确的?( )
A.插入或删除时,无需移动其他元素
B.数据在内存中一定是连续的
C.需要事先估计存储空间
D.可以随机访问表内的元素
练习2
在单链表指针为p的结点之后插入指针为s的结点,正确的操作是( )
A.p->next=s; s->next=p->next;
B.p->next=s->next; p->next=s;
C.s->next=p->next; p->next=s;
D.p->next=s->next; p->next=s;
练习3
在一个单链表中,q 的前一个节点为 p,删除 q 所指向节点时,
以下代码正确的执行语句及次序是( )
①`q->next=p->next;`
②`p->next=q->next;`
③`delete p;`
④`delete q;`
A.②③
B.④
C.①④
D.②④
练习4
在长度为n(n>1)的单链表上,设有头和尾两个指针,执行( )操作与链表的长度有关。
A.在单链表第一个元素前插入一个新元素
B.在单链表最后一个元素后插入一个新元素
C.删除单链表中的第一个元素
D.删除单链表中的最后一个元素
练习5
自己完成下面的接口函数
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLNDataType;
typedef struct SlistNode //single单一的 node节点
{
SLNDataType data;
struct SlistNode* next;
}SLNode;
void SListPrint(SLNode* phead);
void SListPushBack(SLNode** pphead, SLNDataType x);
void SListPushFront(SLNode** pphead, SLNDataType x);
SLNode* CreateNewNode(SLNDataType x);
void SListPopBack(SLNode** pphead);
void SListPopFront(SLNode** pphead);
SLNode* SListFind(SLNode* phead, SLNDataType x);
void SListInsert(SLNode** pphead, SLNode* pos, SLNDataType x);
void SListInsertAfter(SLNode* pos, SLNDataType x);
void SListEarse(SLNode** pphead, SLNode* pos);
void SListEarseAfter(SLNode* pos);
void SListDestroy(SLNode** pphead);
答案及解析
练习1答案:A
解析:链表插入删除元素只需要修改指针指向,不需要移动元素。链表是用指针链接前后节点,数据在内存中不一定连续,每次插入元素,都是先出开一个节点的空间,不需要事先估计存储空间,并且链表没有索引,不能随机访问。
练习2答案:C
解析:先把s和p的next节点链接,再更新p的next为s
练习3答案:D
解析: 首先要更新链接,再删除q节点
练习4答案:D
解析:A选项为头插,不需要遍历链表,B选项为尾插,也不需要遍历链表,C选项为头删,不需要遍历链表,只有D选项,为尾删,需要遍历单链表,找到尾节点的前一个节点。所以与长度有关。
练习5答案参照上面SList.c
本篇完。(附下篇对应OJ链接)
下一篇是链表有关的力扣OJ题,穿越回来贴个链接: