目录
(3)单链表的相关实现(以下代码实现默认无哨兵位,无哨兵位更具有普适性)
一、线性表的链式表示
1.1 单链表
(1)相关概念及特点
单链表指结点中只含一个指针域的链表,也称线性链表。
单链表结点结构下所示,其中data为数据域,存放数据元素;next 为指针域,存放其后继结点的地址。
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
相关特点:
1.利用单链表可以解决顺序表需要大量连续存储单元的缺点。
2.但单链表附加指针域,也存在浪费存储空间的缺点。
3.由于单链表的元素离散地分布在存储空间中,所以单链表是非随机存取的存储结构,查找某个特定的结点时,需要从表头开始遍历, 依次查找。
(2)头结点(哨兵位)
在单链表第一个结点前附设的结点,头指针为NULL时表示一个空表。头结点的数据域可以不设任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点。
图1.1-1 哨兵位设立
头结点和头指针的区分:不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。
引入头结点后,可以带来两个优点:
-
头指针指向头结点,不论链表否为空,头指针总是非空。(链表是否为空的处理均是统一的)
- 头结点的设置使得对链表的开始结点的操作与对表中其它结点的操作一致(均在某一结点之后)。
(3)单链表的相关实现(以下代码实现默认无哨兵位,无哨兵位更具有普适性)
1.链表初始化及其扩容以及摧毁实现
//结构体定义
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//摧毁动态开辟的空间的链表
void SLTNodeDestroy(SLTNode** pphead) //传二级指针的原因是改变其中的元素则头指针需要改变
{
SLTNode* cur = *pphead; //将首元素的指针赋值给cur
while (cur != NULL) //当cur为空时,说明结点被删完了
{
SLTNode* next = cur->next;
free(cur); //释放结点
cur = NULL;
cur = next; //头结点位置变成提前记录的位置
}
}
//动态开辟一个结点大小的空间,来存放数据和下一个结点的地址
//扩容相关操作
SLTNode* BuyListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); //这里用realloc也可以,动态开辟空间
if (newnode == NULL) //开辟失败则结束进程
{
printf("malloc fail\n");
exit(-1);
}
else
{
newnode->data = x; //结点的位置存放一个数据
newnode->next = NULL; //结点指向的下一个地址为空
}
return newnode; //返回开辟好结点的地址
}
//打印单链表中的数据
void SLTNodePrint(SLTNode* pphead)
{
SLTNode* cur = pphead; //避免头结点的随意被改变
while (cur!= NULL) //cur为空的时候停止
{
printf("%d ", cur->data);
cur = cur->next;、
}
printf("\n");
}
2.查找实现
在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止, 否则返回最后一个结点指针域null。
图1.1-2 查找的相关流程
代码实例如下:
在单链表中查找一个数据
SLTNode* SLTNodeFind(SLTNode* pphead, SLTDataType x)
{
SLTNode* cur = pphead;//避免头结点的随意被改变
while (cur != NULL)//cur为空的时候停止
{
if (cur->data == x) //找到要查找的数据
{
return cur;//返回该数据
}
cur = cur->next;
}
return NULL; //没找到
}
3.插入实现
插入结点操作将值为x的新结点插入到单链表的第i个位置上。先检査插入位置的合法性, 然后找到待插入位置的前驱结点,即第i-l个结点,再在其后插入新结点。
图1.1-3 插入的相关流程
代码实例如下:
//在pos位置前插入一个数据
void SLTNodeInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) //不是对pos位置 得数据改变,不用传二级指针
{
assert(pphead);
assert(pos); //判断pos传过来的值不是有效值
SLTNode* newnode = BuyListNode(x);//开辟新节点
if (*pphead == pos) //当第一个结点为pos时
{
newnode->next = *pphead;
*pphead = newnode;
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)//当pos下一个结点为pos时就停止,
{
prev = prev->next;
}
prev->next = newnode;//这时候prev记录pos前一个结点,prev下一个结点指向新节点
newnode->next = pos;//新节点下一个结点指向pos
}
}
4.删除实现
删除结点操作是将单链表的第i个结点删除。先检查删除位置的合法性,后查找表中第i-l 个结点,即被删结点的前驱结点,再将其删除。
图1.1-4 删除的相关流程
代码实现如下:
//删除单链表中pos位置的结点
void SLTNodeErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(*pphead);//判断第一个结点是否为空,为空就没有结点,就不能删
assert(pos); //判断pos传过来的值不是有效值
if (*pphead == pos) //当pos值为头结点时,相当于头删
{
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
else //当pos不指向第一个结点时
{
SLTNode* prev = *pphead;//记录pos前一个结点,这样能够使前后结点连接上
while (prev->next != pos) //prev事项pos前一个结点时结束
{
prev = prev->next;
}
prev->next = pos->next; //pos前一个结点pos的下一个结点
free(pos);//释放pos结点,这样pos位置的结点就删除了,前后链表连接上了
pos = NULL;
}
}
5.头插实现
该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将 新结点插入到当前链表的表头,即头结点之后。
图1.1-5 头插的相关流程
代码实现如下:
//在链表第一个位置前插入一个数据
void SLTNodePushFront(SLTNode** pphead, SLTDataType x)//传二级指针,可能修改第一 个结点
{
SLTNode* newnode = BuyListNode(x); //开辟好要加入的结点
if (*pphead == NULL) //判断链表中没有结点的情况
{
*pphead = newnode; //第一个空结点被赋值成newnode
}
else//当链表中有至少一个结点时
{
newnode->next = *pphead;//新节点的下一个结点指向头结点
*pphead = newnode;//头结点再变成新节点,这时候相当于头插了
}
}
6.尾插实现
头插法建立单链表的算法虽然简单,但生成的链表中结点的次序和输入数据的顺序不一致。 若希望两者次序一致,则可采用尾插法。该方法将新结点插入到当前链表的表尾。
图1.1-6 尾插的相关流程
代码实现如下:
void SLTNodePushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuyListNode(x); //插入数据时要开辟一个空间,用newnode接收这 开辟好空间个地址
if (*pphead == NULL) //*pphead==NULL为空,说明第一个结点没有数据,当链表为空时
{
*pphead = newnode; //这时候把newnode赋值给第一个空结点
}
else
{
//找到尾结点
SLTNode* tail = *pphead; //用一个指针记录尾结点,避免对头结点进行修改
while (tail->next != NULL)//当下一个结点为空,那么当前结点不为空,且为最后一结点
{
tail = tail->next; //tail指针不断指向后面结点的地址
}
tail->next = newnode; //最后一个结点的下一个结点被赋值成要插入的结点
}
}
1.2 双向链表
单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。 要访问某个结点的前驱结点,只能从头开始遍历,访问后继结点的时间复杂度为0(1)但访问前驱结点的时间复杂度为O(N)。
为了克服单链表的上述缺点,引入了双向链表,双向链表结点中有两个指针pre和next,分 别指向其前驱结点和后继结点
图1.2-1双向链表
图1.2-2 双向链表删除
图1.2-3 双向链表插入
图1.2-4 双向链表相关操作
相关实现较单链表更为简单,在这里不做过多赘述了。
1.3 循环链表
循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而改为指向头结点, 从而整个链表形成一个环。
由循环单链表的定义不难推出循环双链表。不同的是在循环双链表中,头结点的prior指针还要指向表尾结点,
图1.3-1 循环单链表
图1.3-2 循环双链表
1.4 静态链表
静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next, 与前面所讲的链表中的指针不同的是,这里的指针是结点的相对地址(数组下标),又称游标。
和顺序表一样,静态链表也要预先分配一块连续的内存空间。
总体来说,静态链表没有单链表使用起来方便,但在一些 不支持指针的高级语言(如Basic)中,是一种非常巧妙的设计方法。
图1.4-1 静态链表相关概念图
图1.4-2 静态链表的优势
二、顺序表与链表的比较
2.1 顺序存储和链式存储典型特点
顺序表可以随时存取表中的任意一个元素,它的存储位置可以用一个简单直观的公式表示,但插入和删除操作需要移动大量元素。
链式存储线性表时,不需要使用地址连续的存储单元, 即不要求逻辑上相邻的元素在物理位置上也相邻,插入和删除操作不需要移动元素,而只需修改指针,但也会失去顺序表可随机存取的 优点。
其相关特点入下:
1.用一组任意的存储单元存储线性表的数据元素
2.利用指针(链)实现了用不相邻的存储单元存放逻辑上相邻的元素
3.每个数据元素除存储本身信息外,还需存储其直接后继的地址。即每个节点包含一个数据域和指针域,数据域指元素本身信息,指针域指示直接后继的存储位置。
图2-1 链式存储和数据存储结构上的区别
2.2 顺序表和链表的具体区别
具体区别如下:
1.存取(读写)方式:
顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。
2.逻辑结构与物理结构
顺序存储逻辑上相邻的元素,对应的物理存储位置也相邻。
链式存储逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示。
3.查找、插入和删除操作
对于按值查找,顺序表无序时,两者的时间复杂度均为O(N)顺序表有序时,可采用折半査找,此时的时间复杂度为O()。
对于按序号査找,顺序表支持随机访问,时间复杂度仅为0(1)。而链表的平均时间复杂度为 0(n)。
顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需 修改相关结点的指针域即可。
4.空间分配
顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充。动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。
链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。