文章目录
- 前言:
- 一.链表的概念及结构
- 1.何为链表
- 2.链表常见的几种形式
- 二.无头单向非循环链表的实现
- 1.定义节点
- 2.创建一个新节点
- 3.单链表打印
- 4.单链表尾插
- 5.单链表尾删
- 6.单链表头插
- 7.单链表头删
- 8.单链表查找
- 9.在pos之前插入
- 10.在pos之前删除
- 11.在pos之后插入
- 12.在pos之后删除
- 13.assert( )断言的使用
- 三.源码
- 1.SList.h
- 2.SList.c
前言:
书接上文,顺序表的问题及思考:
- 中间或头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
为了更好的解决上述问题,本文我们来学习链表:
一.链表的概念及结构
1.何为链表
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
如图为单链表的两种结构:
①物理结构
②逻辑结构
注意:
- 链表的链式结构在逻辑上是连续的,但在物理上不一定连续。
- 现实中的节点一般都是从堆上申请出来的。
- 从堆上申请的节点,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
2.链表常见的几种形式
实际中链表的结构非常多样,各种情况组合起来就有8种链表结构:
①单向或者双向
②带头或者不带头
③循环或者非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
-
无头单向非循环链表
结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
-
带头双向循环链表
结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
二.无头单向非循环链表的实现
函数接口:
typedef int SLTDataType;//定义节点
typedef struct SListNode
{SLTDataType data; //存放数据struct SListNade* next; //指向下一个节点
}SLTnode;//申请节点
SLTnode* BuySLTNode(SLTDataType x);
//打印
void SLTPrint(SLTnode* phead);
//尾插
void SLTPushBack(SLTnode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTnode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTnode** pphead);
//头删
void SLTPopFront(SLTnode** pphead);
//查找
SLTnode* SLTFind(SLTnode* phead, SLTDataType);
//在pos之前插入
void SLTInsert(SLTnode** pphead, SLTnode* pos, SLTDataType x);
//删除pos之前的节点
voide SLTErase(SLTnode** pphead, SLTnode* pos);
//在pos之后插入
void SLTInsertAftter(SLTnode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTnode* pos);
1.定义节点
//节点
typedef struct SListNode
{SLTDataType data; //存放数据struct SListNade* next; //指向下一个节点
}SLTnode;
- 链式结构由一个个的节点链接而成,节点中存放当前的数据data和指向下一个节点的指针。
- 链表正是有节点串联而成,所以只需记住排在最前面的头节点的位置,就能访问链表中的任意一个节点
注:这里的头节点指的是链表中第一个节点,它本身也会存储数据,而后面讲的带头双向循环链表中的头节点仅是链表起始的标志,并不会存储有效数据
2.创建一个新节点
后面我们要在单链表中进行插入和删除,此时的插入删除是针对节点进行的,这个结点是包括SLTDateType数据以及SLTDateType*的指针,因此,为了方便和减少代码的重复度,我们另写一个函数用来专门创建新结点。
SLTnode* BuySLTNode(SLTDataType x)
{SLTnode* newnode = (SLTnode*)malloc(sizeof(SLTnode));if (newnode == NULL){perror("malloc");return;}newnode->data = x;newnode->next = NULL;return newnode;
}
3.单链表打印
void SLTPrint(SLTnode* phead)
{SLTnode* cur = phead; //cur指向phead存放的地址while (cur != NULL) //不为空{printf("%d->", cur->data); //输出当前节点存的值cur=cur->next; //该节点指向下一个节点}printf("NULL\n");
}
cur=cur->next; 这句话的含义是:
4.单链表尾插
//尾插
void SLTPushBack(SLTnode** pphead, SLTDataType x)
{assert(pphead);SLTnode* newnode = BuySLTNode(x);if (*pphead == NULL) //空链表{*pphead = newnode;}else{//找到尾部SLTnode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}
注意:
- 从代码中可以看出,我们使用了二级指针,这是因为我们要在原链表上做出修改,所以要传原链表头节点的地址,也就是一级指针的地址,要用二级指针来接收。
- 然后是找尾过程:
5.单链表尾删
//尾删
void SLTPopBack(SLTnode** pphead)
{assert(pphead);//1.检查是否为空assert(*pphead);//2.只有一个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTnode* tail = *pphead;while (tail->next->next != NULL){tail = tail->next;}free(tail->next);tail->next = NULL;}
}
注意:
- 链表为空时,没东西可删,要特判一下
- 链表只有一个节点时,删完就空了,也特别处理一下
- tail->next->next != NULL这句循环条件的含义是:
6.单链表头插
//头插
void SLTPushFront(SLTnode** pphead, SLTDataType x)
{assert(pphead);SLTnode* newnode = BuySLTNode(x);newnode->next = *pphead;*pphead = newnode;
}
如下图所示:
7.单链表头删
//头删
void SLTPopFront(SLTnode** pphead)
{assert(pphead);//1.检查是否为空assert(*pphead);SLTnode* first = *pphead;*pphead = first->next;free(first);first = NULL;
}
先检查是否为空,如果不为空,则:
直接覆盖掉第一个节点以达到头删的效果。
8.单链表查找
//查找
SLTnode* SLTFind(SLTnode* phead, SLTDataType)
{SLTnode* cur = phead;while (cur){if (cur->data == x) //找到了{return cur;}cur = cur->next;}return NULL; //没找到
}
9.在pos之前插入
//在pos之前插入
void SLTInsert(SLTnode** pphead, SLTnode* pos, SLTDataType x)
{assert(pos); //pos这个节点存在assert(pphead); //正确传值,不能为空if (pos == *pphead) //pos在第一个节点处{SLTPushFront(pphead, x); //复用头插函数}else{//找到pos的前一个位置SLTnode* prev = *pphead;while (prev->next != pos){prev = prev->next;}SLTnode* newnode = BuySLTNode(x);prev->next = newnode;newnode->next = pos;}
}
10.在pos之前删除
//删除pos之前的节点
voide SLTErase(SLTnode** pphead, SLTnode* pos)
{assert(pphead);assert(pos);if (*pphead == pos){SLTPopFront(pphead);}else{//找到pos的前一个位置SLTnode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next; //覆盖掉posfree(pos);}
}
11.在pos之后插入
//在pos之后插入
void SLTInsertAftter(SLTnode* pos, SLTDataType x)
{assert(pos);SLTnode* newnode = BuySLTNode(x);newnode->next = pos->next;pos->next = newnode;
}
12.在pos之后删除
//删除pos之后的节点
void SLTEraseAfter(SLTnode* pos)
{assert(pos);assert(pos->next); //保证pos之后有节点SLTnode* del = pos->next;pos->next = del->next; //覆盖以删除free(del);del = NULL;
}
13.assert( )断言的使用
在上面代码中我们会发现,为什么有的代码需要断言指针,有的就不需要,有的要断言一级指针,有的要断言二级指针,也有的什么都不用断言,这是为什么呢?
- 首先,assert函数在调试的时候非常好用,一旦()内为假,会立刻报错,而且会帮我们提示报错在哪个文件和在哪行代码。
- 一级指针也就是phead,当链表为空的时候,phead就是为NULL,此时需要根据函数要实现的实际功能来确定是否断言。比如:打印函数SLTPrint(),当传来的指针为空时,说明链表中没有元素,但是没有元素也是可以打印的,如果断言就会报错。
- 二级指针永远指向phead,phead的地址是永远存在的,那么pphead就一定不可能为空,所以有**pphead的地方就需要断言pphead,这样可以及时发现在传参时传成空指针的错误。
三.源码
1.SList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef int SLTDataType;//定义节点
typedef struct SListNode
{SLTDataType data; //存放数据struct SListNade* next; //指向下一个节点
}SLTnode;//申请节点
SLTnode* BuySLTNode(SLTDataType x);
//打印
void SLTPrint(SLTnode* phead);
//尾插
void SLTPushBack(SLTnode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTnode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTnode** pphead);
//头删
void SLTPopFront(SLTnode** pphead);
//查找
SLTnode* SLTFind(SLTnode* phead, SLTDataType);
//在pos之前插入
void SLTInsert(SLTnode** pphead, SLTnode* pos, SLTDataType x);
//删除pos之前的节点
voide SLTErase(SLTnode** pphead, SLTnode* pos);
//在pos之后插入
void SLTInsertAftter(SLTnode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTnode* pos);
2.SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"//申请一个新节点
SLTnode* BuySLTNode(SLTDataType x)
{SLTnode* newnode = (SLTnode*)malloc(sizeof(SLTnode));if (newnode == NULL){perror("malloc");return;}newnode->data = x;newnode->next = NULL;return newnode;
}
//打印
void SLTPrint(SLTnode* phead)
{SLTnode* cur = phead; //临时指针while (cur != NULL) //不为空{printf("%d->", cur->data); //输出当前节点存的值cur=cur->next; //该节点指向下一个节点}printf("NULL\n");
}
//尾插
void SLTPushBack(SLTnode** pphead, SLTDataType x)
{assert(pphead);SLTnode* newnode = BuySLTNode(x);if (*pphead == NULL) //空链表{*pphead = newnode;}else{//找到尾部SLTnode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}
//尾删
void SLTPopBack(SLTnode** pphead)
{assert(pphead);//1.检查是否为空assert(*pphead);//2.只有一个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTnode* tail = *pphead;while (tail->next->next != NULL){tail = tail->next;}free(tail->next);tail->next = NULL;}
}
//头插
void SLTPushFront(SLTnode** pphead, SLTDataType x)
{assert(pphead);SLTnode* newnode = BuySLTNode(x);newnode->next = *pphead;*pphead = newnode;
}
//头删
void SLTPopFront(SLTnode** pphead)
{assert(pphead);//1.检查是否为空assert(*pphead);SLTnode* first = *pphead;*pphead = first->next;free(first);first = NULL;
}
//查找
SLTnode* SLTFind(SLTnode* phead, SLTDataType)
{SLTnode* cur = phead;while (cur){if (cur->data == x) //找到了{return cur;}cur = cur->next;}return NULL; //没找到
}
//在pos之前插入
void SLTInsert(SLTnode** pphead, SLTnode* pos, SLTDataType x)
{assert(pos); //pos这个节点存在assert(pphead); //正确传值,不能为空if (pos == *pphead) //pos在第一个节点处{SLTPushFront(pphead, x); //复用头插函数}else{//找到pos的前一个位置SLTnode* prev = *pphead;while (prev->next != pos){prev = prev->next;}SLTnode* newnode = BuySLTNode(x);prev->next = newnode;newnode->next = pos;}
}
//删除pos之前的节点
voide SLTErase(SLTnode** pphead, SLTnode* pos)
{assert(pphead);assert(pos);if (*pphead == pos){SLTPopFront(pphead);}else{//找到pos的前一个位置SLTnode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next; //覆盖掉posfree(pos);}
}
//在pos之后插入
void SLTInsertAftter(SLTnode* pos, SLTDataType x)
{assert(pos);SLTnode* newnode = BuySLTNode(x);newnode->next = pos->next;pos->next = newnode;
}
//删除pos之后的节点
void SLTEraseAfter(SLTnode* pos)
{assert(pos);assert(pos->next); //保证pos之后有节点SLTnode* del = pos->next;pos->next = del->next; //覆盖以删除free(del);del = NULL;
}
本篇到此结束,码文不易,还请多多支持哦!!