线性表有两种存储方式,一种是顺序表,在顺序表的基本操作中,有说过,顺序表是存放在一片连续的内存空间中,以物理位置的相邻来表示逻辑位置的相邻。当数据量过多时,对内存空间的利用率就比较低。
而线性表的另外一种表示就是链表,它没有占据一整片连续的内存,而是分散在一个个零散的区域中,以指针的形式将各个节点连接起来,以表示线性表逻辑上的相邻顺序。这样做可以提高对内存空间的利用率。
链表的类型有很多,根据有无头结点,有无环,双向还是单向一共可以组合为8种类型,接下来要说的是单向不带环,不带头结点的链表的相关操作。
1. 单链表的存储结构
单链表有由一个个的结点构成,通过指针将各节点连接起来,就构成了一条完整的链表。在链表结点中,不仅包含基本的数据,还有就是因为是单链表,所以只需要指向下一个节点的指针即可。存储结构如下:
typedef char LinkListType;//结点所存储数据的数据类型
//定义链表结点结构体
//每个链表节点包含一个数据元素data和指向下一个节点的指针next
typedef struct LinkListNode
{
LinkListType data;//结点的数据域
struct LinkListNode* next;//指向下个结点的指针
}LinkListNode//将链表节点结构体重新命名
2. 链表的定义及初始化
各节点利用指针连接起来形成的链即为链表。只要已知前一个结点的内容,即可根据前一个结点的next域找到下一个节点。所以,只要知道头结点的指针即地址,就可以顺着各节点的next域找到之后的各个结点。因此,可以用头结点的指针来表示该链表。
定义一个单链表就是定义一个头指针,如
LinkListNode* head;//定义指向头结点的头指针head
初始化单链表
初始时,单链表中并没有结点,所以头指针的指向为空。可以简单的写为:
head = NULL;
不过,为提高代码的可维护性,将链表的初始化封装为函数。因为要改变头指针的指向,即改变指针变量的内容,由于形参只是实参的一份临时拷贝,所以,再传参时,要传头指针的地址,即二级指针。
//初始化单链表,这里的phead指的是头指针的地址
void LinkListInit(LinkListNode** phead)
{
if(phead == NULL)
{
//非法输入,对空指针解引用程序会异常终止
return;
}
*phead = NULL;//将头指针*phead的指向置为空
return;
}
3. 在单链表中尾插元素
在链表中尾插元素,不考虑非法输入,首先要申请新结点,使新节点的数据域为所给的数据,指针域统一赋为NULL,在具体使用时再对其指针域进行修改。如果新节点申请成功,返回新节点的地址,如果失败,返回NULL。为提高代码的可维护性,将新节点的申请封装为函数:
//创建新节点,value是新节点的数据域
LinkListNode* CreateNode(LinkListType value)
{
LinkListNode* new_node = (LinkListNode*)malloc(sizeof(LinkListNode));//动态申请节点
new_node->data = value;//使新节点的数据域为value
new_node->next = NULL;//指针域置为空
return new_node;//如果申请成功,返回新节点的地址,如果失败,返回NULL
}
根据链表初始状态的不同,需要考虑以下几种情形:
(1)如果插入之前,链表为空,此时,插入之后,直接使头指针指向新插入的结点即可
(2)若链表不为空,则首先要遍历找到最后一个元素,再将新节点插入最后一个节点的后面
因为对于情形(1),需要改变头指针的指向,所以,函数传参时,传的是头指针的地址,即二级指针。针对上述两种情况,编写代码如下:
//在单链表尾部插入一个新节点,节点的数据为value
void LinkListPushBack(LinkListNode** phead,LinkListType value)
{
if(phead == NULL)
{
//非法输入,对空指针解引用程序会异常终止,所以这里需要判断一下
return;
}
if(*phead == NULL)
{
//空链表,需改变头指针的指向
*phead = CreateNode(value);
return;
}
//链表非空
LinkListNode* cur = *phead;//定义cur为当前节点的指针,初始为头节点的指针即头指针
while(cur->next != NULL)//遍历寻找最后一个节点
{
cur = cur->next;
}
//此时,cur为最后一个节点,所以新节点只需插入在cur之后即可
cur->next = CreateNode(value);
return;
}
该算法的时间复杂度为O(n),空间复杂度为O(1)。
4. 在单链表中尾删结点
因为该链表中的节点都是用malloc动态申请出来的,所以,要对接点进行删除,就要人为的用free去释放。这里将节点的销毁也封装为函数,原因有二:
(1)与节点的申请函数对应,可以提高代码的可读性
(2)也是比较重要的一点,如果该结点的数据也是malloc动态申请出来的,那在释放节点之前就要先释放节点的数据域,在释放该结点,将其封装为函数,可以在销毁函数中增加一条语句,便可以做到一改全改。这样可以增强代码的可维护性。
//销毁单链表中的一个节点node
//这里,封装为函数,可以提高代码的维护性
void LinkListNodeDestory(LinkListNode* node)
{
free(node);//free释放节点
}
根据尾删前链表的状态不同,可分以下几种情形:
(1)如果链表为空,则尾删失败
(2)如果链表中只有一个结点,则尾删后,链表的头指针要指向空
(3)如果链表中不只有一个节点,则就要遍历找到倒数第二个节点,将其next域置为NULL,并将最后一个节点删除
因为在(2)中要改变头指针的指向,所以函数传参时要传递头指针的地址,即二级指针。针对以上几种情形,有以下两种实现方法:
方法1:不需要定义节点变量:
//在单链表尾部删除一个节点(不定义新结点)
void LinkListPopBack1(LinkListNode** phead)
{
if(phead == NULL)
{
//非法输入
return;
}
if(*phead == NULL)
{
//空链表
return;
}
if((*phead)->next == NULL)
{
//只有一个节点,删除后需要改变头指针的指向
LinkListNodeDestory(*phead);//销毁头节点*phead
*phead = NULL;//再使头指针置为空
return;
}
LinkListNode* cur = *phead;//定义当前节点指针cur
while(cur->next->next != NULL)//遍历查找倒数第二个节点
{
cur = cur->next;
}
//此时,cur为倒数第二个节点
LinkListNode* to_delete = cur->next;//保存要删除的结点地址
cur->next = NULL;//将倒数第二个节点的next域置为空
LinkListNodeDestory(to_delete);//销毁最后一个节点to_delete
return;
}
方法2:需定义新节点变量
//定义新节点来尾删
void LinkListPopBack2(LinkListNode** phead)
{
if(phead == NULL)
{
//非法输入
return;
}
if(*phead == NULL)
{
//空链表
return;
}
if((*phead)->next == NULL)
{
//链表中只有一个节点,此时,需要改变头指针的指向
LinkListNodeDestory(*phead);//销毁头节点*phead
*phead = NULL;
return;
}
LinkListNode* cur = (*phead)->next;//定义当前节点查找最后一个节点
LinkListNode* pre = *phead;//定义当前节点的前一个节点
while(cur->next != NULL)
{
pre = cur;//当前节点不是最后一个节点,就使前一个节点先指向当前节点
cur = cur->next;//当前节点在后移
}
//当前节点cur是最后一个节点
pre->next = NULL;//使倒数第二个节点的next域置为空
LinkListNodeDestory(cur);//销毁最后一个节点
return;
}
这两种方法的时间复杂度和空间复杂度均为O(n)和O(1)。
5. 在单链表中头插节点
根据链表的状态不同,可以分为以下两种情况讨论:
(1)初始时,链表为空,此时,只需使头指针指向新节点即可
(2)初始时,链表不为空,此时,需要先使新节点的next域指向原来的头结点,再使头指针指向新节点即可。
因为(1)可以包含在(2)中一起讨论,所以不需要在分情况讨论。头插结点要改变头指针的指向,所以,函数的参数要传头指针的地址,即二级指针。函数的代码如下:
//头插节点,新节点的数据域为value
void LinkListPushFront(LinkListNode** phead,LinkListType value)
{
if(phead == NULL)
{
//非法输入
return;
}
LinkListNode* new_node = CreateNode(value);//创建新节点
new_node->next = *phead;//使新节点的next域指向原来的头节点,如果链表初始为空,即指向NULL
*phead = new_node;//再使头指针指向新节点
return;
}
该算法的时间复杂度和空间复杂度均为O(1)。
6. 在单链表中头删结点
根据初始时链表的状态不同,分为以下一种情形:
(1)链表为空,删除失败
(2)链表只有一个结点,先将头结点释放,在将头结点置为空
(3)链表中不只有一个节点,将头结点的地址保存起来,使头指针指向第二个节点,在根据保存的头结点的地址删除头结点。
同样,(2)(3)可以合并讨论。头删结点要改变头指针的指向,所以,函数的参数要传头指针的地址,即二级指针。函数的代码如下:
//头删节点
void LinkListPopFront(LinkListNode** phead)
{
if(phead == NULL)
{
//非法输入
return;
}
if(*phead == NULL)
{
//空链表
return;
}
LinkListNode* tmp = (*phead)->next;//保存第二个节点的地址,如果链表中只有一个节点,则此时tmp即为NULL
LinkListNodeDestory(*phead);//销毁第一个节点
*phead = tmp;//使头指针指向第二个节点
}
该算法的时间复杂度和空间复杂度均为O(1)。
7. 查找指定元素所在的地址
首先,如果链表为空,则肯定是找不到指定元素的。如果链表不为空,指定的元素可能在该链表中,也可能不在该链表中。这两种情况都要从头遍历链表。如果在,返回指定元素所在的地址,如果不在返回NULL。因为在该算法中,并没有改变头指针的指向,所以函数传参时只需传头指针即以及指针即可。
//查找指定元素to_find所在的地址,如果没找到,返回NULL
LinkListNode* LinkListFind(LinkListNode* head, LinkListType to_find)
{
if(head == NULL)
{
//链表为空,找不到指定元素
return NULL;//直接返回NULL即可
}
//设置cur为当前指针,初始指向头节点
LinkListNode* cur = head;
//遍历链表寻找要查找的元素
while(cur != NULL)
{
if(cur->data == to_find)
{
return cur;//当找到要查找的元素,返回该元素的地址
}
cur = cur->next;//指针后移,继续往后遍历
}
return NULL;//链表遍历完,还没有找到,说明要查找的元素不再该链表中,返回NULL
}
该算法的时间复杂度和空间复杂度分别为O(n)和O(1)。
8. 在任意位置之后插入节点
因为要在指定位置之后插入节点,说明链表中必须至少有一个节点,所以可分为以下情形:
(1)链表为空,插入失败
(2)插入的位置在或不在链表中。此时需要遍历链表来使插入的位置与链表中各节点的地址进行对比,如果相等,则在该结点之后直接插入即可。若遍历完链表还没找到指定的位置,说明该位置不在该链表中,则插入失败。
因为在此过程中,并没有改变头指针的指向,所以函数传参时只需传入头指针即一级指针即可。
//在单链表任意位置pos之后插入节点value
void LinkListInsertAfter(LinkListNode* head,LinkListNode* pos,LinkListType value)
{
if(pos == NULL)
{
//非法输入
return;
}
if(head == NULL)
{
//空链表,插入失败
return;
}
//定义cur为当前指针,初始指向头节点
LinkListNode* cur = head;
//遍历链表,判断插入的位置pos是否在链表中
while(cur != NULL)
{
if(cur == pos)//如果插入的位置在链表中
{
LinkListNode* node = CreateNode(value);//动态申请新节点
node->next = pos->next;//使新节点的next指向pos的下个节点
pos->next = node;//再使pos的next域指向新节点
return;
}
cur = cur->next;//如果没找到插入的位置就一直往后遍历
}
return;//遍历完整个链表都没找到要插入的位置,说明该位置不在该链表中,则插入失败
}
该算法是时间复杂度和空间复杂度分别为O(n)和O(1)。
9. 在任意位置之前插入节点
因为要在指定位置之前插入元素,说明链表中必须要有节点。所以,可分为以下情形:
(1)链表为空,插入失败
(2)如果插入的位置为头结点,即在第一个结点前插入节点,此时相当于头插,需要改变头指针的指向
(3)如果在链表的其他位置之前插入节点,此时需要找到该位置的前一个节点,在进行插入
(4)如果插入的位置不在链表中,则插入失败
因为(2)中需要改变头指针的地址,所以函数传参时传的是头指针的地址即二级指针。代码如下:
//在单链表任意位置pos之前插入节点value
void LinkListInsert(LinkListNode** phead,LinkListNode* pos,LinkListType value)
{
if(phead == NULL || pos == NULL)
{
//非法输入
return;
}
if(*phead == NULL)
{
//空链表,插入失败
return;
}
//如果在第一个节点前插入新节点
if(pos == *phead)
{
//直接调用头插函数
LinkListPushFront(phead,value);
return;
}
//定义cur为当前位置指针来表示指定位置的上一个节点,初始为头指针
LinkListNode* cur = *phead;
//遍历链表,判断插入的位置是否在链表中
while(cur != NULL)
{
if(cur->next == pos)//如果插入的位置在链表中
{
break;//跳出循环
}
cur = cur->next;//还未找到要插入的位置,一直往后遍历
}
if(cur == NULL)//当遍历完整个链表还没找到指定的位置,则插入失败
{
return;
}
LinkListInsertAfter(*phead,cur,value);//此时cur为指定位置的上一个节点,在指定位置之前插入,即在cur之后插入节点
return;
}
该算法的时间复杂度和空间复杂度分别为O(n)和O(1)。
10. 删除任意位置处的节点
根据初始时链表的状态和位置的不同,可分为以下几种情形:
(1)链表为空时,删除失败
(2)当删除的位置为第一个结点时,此时需改变头指针的指向,相当于头删
(3)当删除的位置在链表中其他节点时,不需要改变头指针的指向
(4)当删除的位置不在链表中时,删除失败。
代码如下:
//删除任意位置处的节点
void LinkListErase(LinkListNode** phead, LinkListNode* pos)
{
if(phead == NULL || pos == NULL)
{
//非法输入
return;
}
if(*phead == NULL)
{
//链表为空,删除失败
return;
}
if(pos == *phead)
{
//删除位置为第一个节点
*phead = pos->next;//使头指针指向第二个节点
LinkListNodeDestory(pos);//删除头节点
return;
}
//定义cur为当前指针变量来表示指定位置的上一个节点,初始为头节点
LinkListNode* cur = *phead;
while(cur != NULL)
{
if(cur->next == pos)//如果删除的位置在链表中
{
break;
}
cur = cur->next;//还没找到要是删除的位置,就继续往后遍历
}
if(cur == NULL)//如果遍历完整个链表还没找到指定位置,说明指点位置不再链表中
{
return;
}
//删除的位置在链表中,cur表示指定位置的上一个节点
cur->next = pos->next;//使cur指向指定位置的下一个节点
LinkListNodeDestory(pos);//删除指定位置出的节点
return;
}
该算法的时间复杂度和空间复杂度分别为O(n)和O(1)。
11. 删除指定值的结点(如果有多个指定值,只删除一个)
根据指定的值删除节点,可分为以下几种情形
(1)链表为空,删除失败
(2)要删除的结点为头结点,此时需要修改头指针的指向
(3)要删除的结点为后面的结点,此时,需要遍历寻找要删除的结点
(4)链表中没有要删除的值,此时,需要遍历完整个链表才知道。
代码如下:
//删除指定元素to_delete的节点,如果有多个,只删除一个
void LinkListRemove(LinkListNode** phead,LinkListType to_delete)
{
if(phead == NULL)
{
//非法输入
return;
}
if(*phead == NULL)
{
//空链表
return;
}
if((*phead)->data == to_delete)
{
//删除头节点,此时,需要改变头指针的指向
LinkListNode* tmp = (*phead)->next;//保存要删除节点的下一个节点的地址
LinkListNodeDestory(*phead);//删除要删除的节点
*phead = tmp;//使头指针指向下个节点
return;
}
//定义cur为当前节点的地址,初始为头指针
LinkListNode* cur = *phead;
while(cur->next != NULL)//遍历查找要删除的节点
{
if(cur->next->data == to_delete)//如果找到了
{
LinkListNode* tmp = cur->next->next;//保存要删除节点的下一个节点的地址
LinkListNodeDestory(cur->next);//删除要删除的节点
cur->next = tmp;//使cur的next指向下个节点
return;
}
cur = cur->next;//没找到,继续往后遍历
}
return;//当遍历完链表还没找到,说明链表中不存在该节点
}
在此例中,还可以通过上述已有的函数来实现。
(1)先通过LinkListFind函数找到要删除结点所在的位置
(2)再根据找到的位置利用LinkListErase函数将该结点删除。
12. 删除指定值的结点(如果有多个指定值,则全部删除)
可以通过上述已有的函数来实现。
(1)先通过LinkListFind函数找到要删除结点所在的位置
(2)再根据找到的位置利用LinkListErase函数将该结点删除
(3)如果有多个,就一直循环(1)(2)两过程,直到找不到要删除的结点,即删除成功
代码如下:
//删除指定元素的所有节点
void LinkListRemoveAll(LinkListNode** phead,LinkListType to_delete)
{
if(phead == NULL)
{
//非法输入
return;
}
//找到删除节点的位置(一次只能找一个)
LinkListNode* pos = LinkListFind(*phead,to_delete);
while(pos != NULL)//如果链表中还有要删除的节点
{
LinkListErase(phead,pos);//根据节点的位置将其删除
pos = LinkListFind(*phead,to_delete);//继续查找指定元素的下一个节点所在位置
}
return;
}
13. 链表判空
如果链表的头指针指向NULL,即链表为空,返回1
如果链表的头指针指向NULL,即链表非空,返回0
代码如下:
//链表判空,如果为空,返回1,否则返回0
int LinkListEmpty(LinkListNode* head)
{
if(head == NULL)
{
//链表为空
return 1;
}
return 0;
}
14. 统计链表中的元素个数
(1)当链表的头指针指向NULL时,链表为空
(2)否则,定义当前节点的指针变量cur和计数器变量count,依次往后遍历,移动一次,count加1,直到cur指向NULL
代码如下:
//统计链表中的节点个数
size_t LinkListSize(LinkListNode* head)
{
//头指针指向NULL,链表为空
if(head == NULL)
{
return 0;
}
size_t count = 0;//定义计数器变量用于记录节点的个数,初始为0
LinkListNode* cur = head;//定义当前指针变量,初始指向头节点
while(cur != NULL)//当cur不为空时
{
count++;//节点个数加1
cur = cur->next;//cur后移
}
return count;
}
另外,上述代码中的链表为空语句可以包含在后面的语句中,所以可以省略不写。
15. 取链表中最后一个节点的地址
(1)链表为空时,最后一个节点的地址即为NULL
(2)链表不为空时,要遍历找到最后一个节点,而最后一个节点的判断依据是它的next域为NULL
代码如下:
//取链表最后一个节点的地址
LinkList* LinkListBack(LinkListNode* head)
{
if(head == NULL)
{
//链表为空
return NULL;
}
LinkList* cur = head;//定义cur为当前指针变量,初始指向头节点
while(cur->next != NULL)//当为找到最后一个节点时,指针一直往后移,直到找到为止
{
cur = cur->next;
}
//此时,cur为最后一个节点的地址
return cur;
}
16. 销毁链表
可分为以下几种情况:
(1)链表为空,不用再销毁
(2)链表非空,从头结点开始遍历,一个一个进行销毁
代码如下:
//销毁链表
void LinkListDestory(LinkListNode** phead)
{
if(phead == NULL)
{
//非法输入
return;
}
if(*phead == NULL)
{
//链表为空
return;
}
//定义cur为当前指针变量,初始指向头节点
LinkListNode* cur = *phead;
while(cur != NULL)//如果cur不为NULL,就要对其进行销毁
{
LinkListNode* to_delete = cur;//保存要销毁的节点地址
cur = to_delete->next;//使cur指向要销毁节点的下一个节点
DestoryNode(to_delete);//销毁节点
}
*phead = NULL;//全部销毁结束后,使头指针置为NULL
return;
}