链表是针对顺序表的缺陷而设计的。
1.顺序表扩容存在一定的空间浪费——链表按需扩容
2.头部和中间的删除和插入都需要挪动数据,时间复杂度O(N)——链表物理空间不连续,不用挪动
单链表SList(Single List)
通过下面对单链表的实现,可以发现单链表的缺陷也有很多,但学单链表还是有很大意义的:
链表笔试面试题,基本都是单链表结构;
单链表以后会作为其他数据结构的子结构,如哈希桶、图的邻接表。
基本结构
链表是由一个个结点链接而成,每个结点包含:1.存储的数据;2.指向下一个结点的指针。
//.h头文件中
typedef int SLTDataType;
typedef struct SListNode{ //Node:结点
SLTDataType data;
struct SListNode* next;
}SLTNode;
void Test1(){ //简单写几个结点并链接起来成为一条链表
SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n1);
SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n2);
SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n3);
SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n4);
n1->data = 1;
n2->data = 2;
n3->data = 3;
n4->data = 4;
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = NULL;
SListPrint(n1);//打印
}
打印链表数据
void SListPrint(SLTNode* phead){//phead:第一个结点的地址
//assert(phead != NULL);
SLTNode* cur = phead;//保留头部指针,只让cur指针移动。cur:current,表示当前指针。
while (cur != NULL){
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
新建结点
由于多个接口都需要新建结点,故将此逻辑封装为另外的接口,方便代码复用。
SLTNode* CreatSListNode(SLTDataType x){
SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newNode != NULL);
newNode->data = x;
newNode->next = NULL;
return newNode;
}
尾插数据
尾插数据接口有两种情况:
链表尚未有结点;——我们直接给它一个结点即可
链表已有结点。——让尾结点的next指向新结点即可
在第一种情况下,由于改变了指针变量,因此接口接收的参数应该为二级指针,否则,形参的改变不影响实参。
第二种情况的思路:
void SListPushBack(SLTNode** pphead,SLTDataType x){
//因为需要对指向结点的指针进行改变,所以要用到二级指针。
assert(pphead != NULL);
SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newNode != NULL);
newNode->data = x;
newNode->next = NULL;
if (*pphead == NULL){ //当传入空指针时
*pphead = newNode;
}
else{
SLTNode* tail = *pphead;
while (tail->next != NULL){
tail = tail->next;
}
tail->next = newNode;
}
}
void test(){
SLTNode* n1 = NULL;
SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
assert(n2);
SListPushBack(&n1,666); //要想改变n1,就得传n1的地址
SListPushBack(&n2,666);
}
头插数据
头插也是两种情况。
void SListPushFront(SLTNode** pphead, SLTDataType x){
assert(pphead != NULL);
SLTNode* newNode = CreatSListNode(x);
newNode->next = *pphead;
*pphead = newNode;
}
头删数据
void SListPopFront(SLTNode** pphead){
assert(pphead != NULL);
assert(*pphead != NULL);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
尾删数据
不能简单粗暴地直接free掉尾结点,否则造成倒数第二个结点的next指向一个不存在的空间(野指针)。
可以用到双指针的思想。
但同时要注意双指针容易造成空指针的问题,比如下面的代码,如果只考虑多个结点的情况,当链表只剩下1个结点时,preTail->next = NULL会报空指针的错误,因为preTail此时是空指针,对空指针解引用->会出错。
void SListPopBack(SLTNode** pphead){
assert(pphead != NULL);
assert(*pphead != NULL);
//链表只有1个结点
if ((*pphead)->next == NULL){
free((*pphead)->next);
*pphead = NULL;
}
else{//链表有多个结点
SLTNode* tail = *pphead;
SLTNode* preTail = NULL;
while (tail->next != NULL){
preTail = tail;
tail = tail->next;
}
free(tail);
preTail->next = NULL;
}
}
//另一种写法
void SListPopBack(SLTNode** pphead){
assert(*pphead != NULL);
//链表只有1个结点
if ((*pphead)->next == NULL){
free((*pphead)->next);
*pphead = NULL;
}
else{//链表有多个结点
SLTNode* tail = *pphead;
while(tail->next->next != NULL){
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
查询数据
单链表中的查找,返回值设置成SLTNode的指针,这样就可以利用返回值进行:
修改找到的值;
配合Insert和Erase接口使用;
SLTNode* SListFind(SLTNode* phead, SLTDataType x){
assert(phead != NULL);
SLTNode* cur = phead;
while (cur){
if (cur->data == x)
return cur;
else
cur = cur->next;
}
return NULL;
}
//test.c
void Test3(){
SLTNode* n1 = NULL;
SListPushFront(&n1, 3);
SListPushFront(&n1, 2);
SListPushFront(&n1, 3);
SListPushFront(&n1, 3);
SListPushFront(&n1, 1);
SListPrint(n1);
SLTNode* ret = SListFind(n1, 3);
if (ret){
printf("找到了,把它修改为6\n");
ret->data = 6;
}
else{
printf("没找到\n");
}
SListPrint(n1);
}
在定义接口时,如果指针一定不允许为空,但用户有可能传空值的时候,记得assert(ptr)断言一下,这样如果用户误传了个NULL的指针过来,程序能很快找到错误地点,方便排查错误。
在特定位置前插入新结点
配合查询SListFind( )使用。
//任意位置插入(在pos结点前插入新结点,数据为x)
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x){
assert(pphead != NULL);
assert(pos != NULL); //检查了pos不为空,就相当于链表不是空的。
if (pos == *pphead){ //头插
SListPushFront(pphead, x);
}
else{
//找到pos结点的前一个结点
SLTNode* prev = *pphead;
while (prev->next != pos){
prev = prev->next;
}
SLTNode* newNode = CreatSListNode(x);
prev->next = newNode;
newNode->next = pos;
}
}
void Test4(){
SLTNode* n1 = NULL;
SListPushFront(&n1, 5);
SListPushFront(&n1, 4);
SListPushFront(&n1, 3);
SListPushFront(&n1, 2);
SListPushFront(&n1, 1);
SListPrint(n1);//1->2->3->4->5->NULL
SLTNode* pos = SListFind(n1, 4);
SListInsert(&n1,pos,666);
SListPrint(n1);//1->2->3->666->4->5->NULL
}
删除特定位置结点
这个也是要配合查询SListFind( )使用。也是分为头删和非头删两种情况。
void SListErase(SLTNode** pphead, SLTNode* pos){
assert(pphead);
assert(pos); //检查了pos不为空,就相当于链表不是空的。
if (pos == *pphead){
SListPopFront(pphead);
}
else{
//找到pos结点的前一个结点
SLTNode* prev = *pphead;
while (prev->next != pos){
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
可以看到,在pos位置之前插入/删除数据,需要遍历一遍链表以找到pos结点的前一个结点,时间复杂度是O(N),没有解决顺序表挪动数据的问题。
因此在特定位置前插入/删除,不适合用单链表解决。
在C++官方实现的STL中的单链表forward_list中实现的插入删除,都是在pos位置之后插入/删除。
在特定位置后插入新结点
时间复杂度O(1)
void SListInsertAfter(SLTNode* pos, SLTDataType x){
assert(pos);
SLTNode* newNode = CreatSListNode(x);
newNode->next = pos->next;//注意顺序
pos->next = newNode;
}
删除特定位置后一个结点
时间复杂度O(1)
void SListEraseAfter(SLTNode* pos){
assert(pos);
if (pos->next == NULL){
return;
}
else{
SLTNode* tmp = pos->next->next;
free(pos->next);
pos->next = tmp;
}
}