目录
在学习链表之前,我们先说说之前学的顺序表,在前面的章节中,我们说,顺序表实际上是对数组的操纵,它的内存空间是连续的,可以通过数组下标随机访问,但是链表就不行,这是顺序表的优点,那它有没有缺点呢?
答案是有的。
顺序表的问题:
1. 中间/头部的插入删除,时间复杂度是O(N)。
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容 到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
如何解决上述问题呢?下面我们来学习链表。
1. 链表
1.1 链表的概念和结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
顺序表的问题本质就在于,它是一块连续的物理空间,而链表就不一样了。链表在内存中申请的是一块一块的内存空间,相互之间通过节点联系在一起。
开始时给一个指针指向第一个空间,然后通过节点找到第二个,第二个找到第三个........最后一个节点指向空(NULL)
如下图:
现在我们首先来定义一个结构体:
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLTNode;
结构体中有一个数据data,还有一个结构体指针next。
下面我们简单来遍历打印一个链表:
void SLDPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
上述代码中,把头节点赋给cur,cur->data是结构体中的数据,代码中的cur=cur->next刚开始可能有点难以理解,实际上cur->next访问的是存放在结构体中的下一个结构体的地址,->相当于对其进行解引用,这样cur就可以找到下一个节点,然后打印下一个结构体的数据,直到最后一个节点的cur->next指向空指针(NULL)。
上图其实就是链表的逻辑结构,这是为了方便形象理解,想象出来的。实际上链表在内存中是下面的物理结构:
实际的物理结构,变量phead中存放的是第1个节点的地址,第1个节点的结构体中存放的是第2个节点的地址, 2中存放3节点的地址,3中存放4节点的地址.......直到最后的节点中存放的是空。
这样上述代码中的cur = cur->next就更好理解了,它实际上相当把next中的地址拷贝到cur中去,然后cur就指向了下一个节点。
1.2 链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向:
2. 带头或者不带头
3. 循环或者非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
2. 单链表的实现:
单链表头插数据:
头插数据只需要使新开辟出来的newnode节点下一个指向的是头节点phead,然后把头节点指向在newnode的地方。
下面我们来看看下面代码实现:
//单链表头插数据
void SLPushFront(SLTNode* phead, SLDataType x)
{
//动态内存开辟
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc");
return;
}
newnode->data = x;
newnode->next = NULL;
//头插
newnode->next = phead;
phead = newnode;
}
问题来了,上述代码正确吗?
看起来逻辑十分严密是吧,但是我们测试运行一下就会发现,什么都没有打印出来:
这又是为什么呢?
因为我们在头插的时候改变的是指针,例如:newnode->next = phead; phead = newnode;改变指针实际上就是,把指针的值拷贝过去,当我们传plist,并用phead接收时,实际上就是phead把plist的值拷贝过去,但是后面phead = newnode,又把newnode的值拷贝过去了,原有的值被覆盖了,plist和phead建立不上联系。
那该怎么办呢?
很简单,既然我们要改变指针,那就传指针的地址,用二级指针接收,使用时解引用就行。通过*phead=newnode可以使newnode和plist直接建立联系。
代码修改如下:
//单链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x)
{
//动态内存开辟
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc");
return;
}
newnode->data = x;
newnode->next = NULL;
newnode->next = *phead;
*phead = newnode;
}
测试:
单链表尾插数据
尾插数据,因为链表不是连续物理空间,所以我们要先找到链表的尾,然后再指向新开辟的节点newnode,并把newnode->next置为NULL。
同理,尾部插入也需要使用二级指针,如果不使用二级指针,在phead=newnode这条语句中,因为此时的newnode和phead是局部变量,出了作用域就销毁了,所以phead和plist建立不上联系。而如果我们用二级指针,*phead=newnode,相当于把newnode的值直接给plist,尽管phead和newnode后面还会销毁,但是新节点和plist的联系已经建立起来了。
不论是头插还是尾插数据,都要开辟新空间,所以我们可以将其封装为一个函数:
//动态内存开辟
SLTNode*CheckSList(SLDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
尾插数据代码:
//单链表尾插数据
void SLPushBack(SLTNode** phead, SLDataType x)
{
SLTNode* newnode = CheckSList(x);
//1.空链表
//2.非空链表
if (*phead == NULL)
{
*phead = newnode;
}
else
{
SLTNode* tail = *phead;
//找到尾
while (tail->next != NULL)
{
tail = tail->next;
}
//尾插
tail->next = newnode;
}
}
注意:要判断链表是不是空链表。
测试:
单链表头删数据:
要想头删数据,只需要将头节点指向第二个节点,并将头节点free掉,置为NULL。
注意:分三种情况,无节点,一个节点和多个节点。实现的方式不同,需要判断。
//单链表头删数据
void SLPopFront(SLTNode** phead)
{
assert(*phead);
//一个节点
//多个节点
if ((*phead)->next == NULL)
{
free(*phead);
*phead = NULL;
}
else
{
SLTNode* start = *phead;
*phead = start->next;
free(start);
start = NULL;
}
}
测试:
单链表尾删数据:
要想尾删数据,可以先找到倒数第二个节点,把它后面的节点free掉,并置为NULL即可。
注意:分三种情况,无节点,一个节点和多个节点。实现的方式不同,需要判断。
代码如下:
//单链表尾删数据
void SLP0pBack(SLTNode** phead)
{
assert(*phead);
//一个节点
//多个节点
if ((*phead)->next == NULL)
{
free(*phead);
*phead = NULL;
}
else
{
SLTNode* tail = *phead;
//找到倒数第二个节点
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
测试:
单链表查找并修改数据:
查找数据很简单,直接把链表遍历一遍就行。而查找和修改可以同时实现。
直接上代码:
//单链表查找数据
SLTNode* SLFind(SLTNode* phead, SLDataType x)
{
assert(phead);
SLTNode* cur = phead;
int count = 0;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
上述代码可以同时实现查找和修改,查找到数据后,返回节点指针,通过指针修改结构体数据:
SLTNode* pos = SLFind(plist,2);
if (pos != NULL)
pos->data = 30;
测试:
在上文中,我们发现有些参数传的是一级指针,有些传的是二级指针,为什么呢?
其实给打印和查找函数传的就是一级指针,而其他的头插、尾插、头删、尾删都是二级指针,原因很明显,打印和查找函数不需要改变指针,而其他的需要改变指针,要改变指针就要使用二级指针。
下面我们再来补充一个内容,
什么时候需要断言?
比如:查找函数需要断言assert(phead)吗?
不需要,因为即使phead传过来的是空链表,查找函数也可以找,找不到就返回NULL。同理,打印函数也不需要断言,它可以打印空链表NULL。
头插函数中需要断言assert(phead)吗?
需要,因为phead是头指针plist的地址,永远都不能为空,一旦为空,再对其解引用就成空指针NULL了。
头插函数中需要断言assert(*phead)吗?
不需要,因为它即使为空,空链表也可以插入数据啊。
头插函数断言如下:
//单链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x)
{
assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
//assert(*phead);不需要断言,链表为空,也需要能插入
SLTNode*newnode=CheckSList(x);
newnode->next = *phead;
*phead = newnode;
}
头删函数中需要断言assert(phead)和assert(*phead)吗?
都需要,头删函数中,一旦链表为空就不能再删了,所以需要断言assert(*phead),而断言assert(phead)的原因和上文一致。
头删函数断言如下:
//单链表头删数据
void SLPopFront(SLTNode** phead)
{
assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
assert(*phead);//链表为空,不能再删。
//一个节点
//多个节点
if ((*phead)->next == NULL)
{
free(*phead);
*phead = NULL;
}
else
{
SLTNode* start = *phead;
*phead = start->next;
free(start);
start = NULL;
}
}
同理,尾删和头删保持一致,尾插和头插保持一致。
下面我们接着讲链表任意位置的插入和删除:
单链表在pos之前插入数据:
要想在pos之前插入数据,就要先找到pos前一个节点,然后把使该节点下一个指向newnode,让newnode的下个节点指向pos。
代码如下:
//单链表在pos之前插入数据
void SLInsert(SLTNode** phead, SLTNode* pos, SLDataType x)
{
assert(phead);
assert(pos);
if (*phead == pos)
{
SLPushFront(phead, x);
}
else
{
SLTNode* newnode = CheckSList(x);
SLTNode* cur = *phead;
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = newnode;
newnode->next = pos;
}
}
注意:上述代码中复用了头插函数,因为当pos在头节点的位置时,while循环就找不到pos的前一个节点了,所以这种情况需要单独判断,当pos在头节点位置时,直接使用头插。
测试:先用查找函数找到pos,然后在pos前面插入数据:
单链表在pos之后插入数据:
在pos之后插入数据很简单,直接插入就行。
上代码:
//单链表在pos后插入数据
void SLErase(SLTNode** phead, SLTNode* pos, SLDataType x)
{
assert(phead);
SLTNode* newnode = CheckSList(x);
SLTNode* next = pos->next;
pos->next = newnode;
newnode->next = next;
}
测试:也是先用查找函数找到pos,然后在pos后面插入数据:
单链表在pos位置删除数据:
经过上文的练习,这个应该很简单吧。直接找到pos前一个节点,让它指向pos下一个节点,然后将pos节点free就行。
上代码:
//单链表在pos位置删除数据
void SLPErase(SLTNode** phead, SLTNode* pos)
{
assert(phead);
assert(*phead);
if (*phead == pos)
{
SLPopFront(phead);
}
else
{
SLTNode* prev =*phead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
测试:
至于在pos之前和之后删除数据,和前面的思路一样,不再赘述。
3.完整代码:
test.c:
仅供测试用例。
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//测试函数
void test1()
{
SLTNode* plist = NULL;
SLPushFront(&plist, 1);
SLPushFront(&plist, 2);
SLPushFront(&plist, 3);
SLPushFront(&plist, 4);
SLTNode* pos = SLFind(plist, 2);
if (pos != NULL)
SLPErase(&plist, pos);
/*SLPushBack(&plist, 5);
SLPushBack(&plist, 6);
SLPushBack(&plist, 7);*/
/*SLTNode* pos = SLFind(plist,2);
if (pos != NULL)
pos->data = 30;*/
/*SLPopFront(&plist);
SLPopFront(&plist);
SLPopFront(&plist);*/
SLPrint(plist);
}
int main()
{
test1();
return 0;
}
SList.h:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义结构体
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLTNode;
//打印函数
void SLPrint(SLTNode*phead);
//单链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x);
//单链表尾插数据
void SLPushBack(SLTNode** phead, SLDataType x);
//单链表头删数据
void SLPopFront(SLTNode** phead);
//单链表尾删数据
void SLP0pBack(SLTNode** phead);
//单链表查找数据
SLTNode* SLFind(SLTNode* phead, SLDataType x);
//单链表在pos之前插入数据
void SLInsert(SLTNode** phead, SLTNode* pos, SLDataType x);
//单链表在pos之后插入数据
void SLErase(SLTNode** phead, SLTNode* pos, SLDataType x);
//单链表在pos位置删除数据
void SLPErase(SLTNode** phead, SLTNode* pos);
SList.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void SLPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//动态内存开辟
SLTNode*CheckSList(SLDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x)
{
assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
//assert(*phead);不需要断言,链表为空,也需要能插入
SLTNode*newnode=CheckSList(x);
newnode->next = *phead;
*phead = newnode;
}
//链表尾插数据
void SLPushBack(SLTNode** phead, SLDataType x)
{
assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
//assert(*phead);不需要断言,链表为空,也需要能插入
SLTNode* newnode = CheckSList(x);
//1.空链表
//2.非空链表
if (*phead == NULL)
{
*phead = newnode;
}
else
{
SLTNode* tail = *phead;
//找到尾
while (tail->next != NULL)
{
tail = tail->next;
}
//尾插
tail->next = newnode;
}
}
//链表头删数据
void SLPopFront(SLTNode** phead)
{
assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
assert(*phead);//链表为空,不能再删。
//一个节点
//多个节点
if ((*phead)->next == NULL)
{
free(*phead);
*phead = NULL;
}
else
{
SLTNode* start = *phead;
*phead = start->next;
free(start);
start = NULL;
}
}
//链表尾删数据
void SLP0pBack(SLTNode** phead)
{
assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
assert(*phead);//链表为空,不能再删。
//一个节点
//多个节点
if ((*phead)->next == NULL)
{
free(*phead);
*phead = NULL;
}
else
{
SLTNode* tail = *phead;
//找到倒数第二个节点
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
//链表查找数据
SLTNode* SLFind(SLTNode* phead, SLDataType x)
{
assert(phead);
SLTNode* cur = phead;
int count = 0;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//单链表在pos之前插入数据
void SLInsert(SLTNode** phead, SLTNode* pos, SLDataType x)
{
assert(phead);
assert(pos);
if (*phead == pos)
{
SLPushFront(phead, x);
}
else
{
SLTNode* newnode = CheckSList(x);
SLTNode* cur = *phead;
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = newnode;
newnode->next = pos;
}
}
//单链表在pos后插入数据
void SLErase(SLTNode** phead, SLTNode* pos, SLDataType x)
{
assert(phead);
SLTNode* newnode = CheckSList(x);
SLTNode* next = pos->next;
pos->next = newnode;
newnode->next=next;
}
//单链表在pos位置删除数据
void SLPErase(SLTNode** phead, SLTNode* pos)
{
assert(phead);
assert(*phead);
if (*phead == pos)
{
SLPopFront(phead);
}
else
{
SLTNode* prev =*phead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
以上就是今天学习的关于单链表的所有内容。
未完待续。。。