动态单链表的实现
1 单链表存储结构代码描述
若链表没有头结点,则头指针是指向第一个结点的指针。
若链表有头结点,则头指针是指向头结点的指针。
空链表的示意图:
带有头结点的单链表:
不带头结点的单链表的存储结果示意图:
在C语言中可用结构指针来描述单链表:
/* 线性表的单链表存储结构 */
typedef struct node
{
ElemType data;
struct node *next;
}Node;
typedef Node* LinkList; //定义单链表指针
从这个结构定义中可以看出,结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
2 单链表的读取
在单链表中,由于第i个元素到底在哪?没办法一开始就知道,必须得从头开始找。
获得链表第i个数据的算法思路:
- 声明一个结点指针cur指向链表第一个结点,初始化j从1开始;
- 当 j<i 时,就遍历链表,让cur的指针向后移动,不断指向下一结点,j 累加1;
- 若到链表末尾cur为空,则说明第i个元素不存在;
- 否则查找成功,返回结点cur的数据。
注意:cur最开始指向第一个结点,而不是头结点,cur与j同步。
/******************************************
名称: 获取元素操作
功能: 获取链表第i个结点数据,赋给e
返回值: 查找成功返回true,否则返回false
******************************************/
Status GetElem(LinkList pList, int i, ElemType *e)
{
LinkList cur;
cur = pList->next; //让cur指向链表pList的第1个结点
int j = 1; //j为计数器,赋值为1,对应cur指向结点
//cur不为空或者计数器j还没有等于i时,循环继续
while (cur!=NULL && j < i) //若i=1,则不用进入循环遍历,就可找到了
{
cur = cur->next;
j++;
}
if (cur == NULL || j>i) //j>i,为了避免输入i小于1的情况
return FALSE; //到链表尾,仍未找到第i个元素
*e = cur->data; //取第i个元素的数据
return TRUE;
}
说白了,就是从头开始找,直到第 i 个元素为止。由于这个算法的时间复杂度取决于 i 的位置,当 i=1 肘,则不用进入循环遍历,第一个就取出数据了,而当 i=n 时则遍历 n-1
次才可以。 因此最坏情况的时间复杂度是O(n)。
由于单链表的结构中没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用for来控制循环。其主要核心思想就是"工作指针后移',这其实也是很多算法的常用技术。
3 单链表的插入与删除
单链表的插入操作核心代码只有两句(在结点p后面插入结点s):
s->next = p->next; /* 将p的后继结点赋值给s的后继 */
p->next = s; /* 将s赋值给p的后继 */
解读这两句代码,也就是说让p的后继结点改成s的后继结点,再把结点s变成p的后继结点,如下图所示:
对于单链裴的表头和表尾的特殊情况,操作是相同的,如下图所示:
单链表第i个位置插入结点的算法思路:
- 声明一结点p指向链表头结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j 累加1;
- 若到链表末尾p为空,则说明第 i 个元素不存在i;
- 否则查找成功,在系统中生成一个空结点s;
- 将数据元素e赋值给s->data;
- 执行单链表的插入结点语旬s->next=p->next p->next=s;
- 返回成功。
注意:front最开始指向头结点,而不是第一个结点,而j赋值为1,让front与j不同步,front始终指向i位置前一个结点。
实现代码算法如下:
/******************************************
名称: 插入元素操作
功能: 在链表第i个位置插入新的数据元素e
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsert(LinkList pList, int i, const ElemType e)
{
LinkList front; //指向插入位置前一个结点
LinkList pTemp;
int j;
front = pList; //让front指向链表pList的头结点
j = 1; //j为计数器,赋值为1,让front能指向插入位置前一个结点
//找到i位置所在的结点
while (front != NULL && j < i)
{
front = front->next;
j++;
}
if (front == NULL || j>i) //j>i,为了避免输入i小于1的情况
return FALSE; //到链表尾,仍未找到第i个元素
pTemp = (LinkList)malloc(sizeof(Node));
pTemp->data = e;
//插入结点s
pTemp->next = front->next;
front->next = pTemp;
return TRUE;
}
4 单链表的删除
单链表的删除操作核心代码只有一句(删除结点p后面一个结点):
p->next = p->next->next; /* 将p的后继结点的后继赋值给p的后继 */
单链表第 i 个数据之后删除结点的算法思路(删除的是i+1位置的结点):
- 声明一结点p指向链表头结点, 初始化 j 从1开始;
- 当 j<i 时,就遍历链表,让front的指针向后移动,不断指向下一个结点,j 累加1;
- 若到链表末尾front为空,则说明第 i 个元素不存在;
- 否则查找成功,查找到要删除位置的前一个结点front,并赋值给q;
- 执行链表的删除结点语句front->next = front->next->next;
- 将q结点中的数据赋值给e, 作为返回;
- 释放q结点,并指向NULL;
- 返回成功。
注意:front最开始指向头结点,而不是第一个结点,而j赋值为1,让front与j不同步,front始终指向i位置前一个结点。
实现代码算法如下:
/******************************************
名称: 删除元素操作
功能: 在链表第i个位置删除结点
返回值: 删除成功返回true,否则返回false
******************************************/
Status ListDelete(LinkList pList, int i, ElemType *e)
{
LinkList front, pTemp;
front = pList; //让front指向链表pList的头结点
int j = 1; //j为计数器,赋值为1,让front能指向插入位置前一个结点
//找到i位置所在的结点
while (front != NULL && j < i)
{
front = front->next;
j++;
}
if (front == NULL || j > i) //j>i,为了避免输入i小于1的情况
return FALSE; //到链表尾,仍未找到第i个元素
LinkList q = front->next;
//删除结点
front->next = front->next->next;
//使用q结点,保存已经删除的结点
*e = q->data; //将要删除结点的数据赋给e
free(q);
q = NULL;
return TRUE;
}
分析一下刚才我们讲解的单链表插入和删除算法,我们发现,它们其实都是由两部分组成;第一部分就是遍历查找第i个元素;第二部分就是插入和删除元素。从整个算法来说,我们很容易推导出:它们的时间复杂度都是O(n)。
从第i个位置插入10个元素,对于顺序存储结构意味着,每一次插入都需要移动 n-i 个元素,每次都是O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。
5 单链表的头部插入与尾部插入
头部插入,就是始终让新结点在第一个结点的位置,这种算法简称为头插法,如下图所示:
实现代码算法如下:
/******************************************
名称: 头部后插入元素操作
功能: 在链表头部后插入存储数据元素的结点
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsertHead(LinkList pList, const ElemType e)
{
LinkList head, pTemp;
//检测链表是否存在
if (!pList)
{
printf("pList not exist!\n");
return FALSE;
}
pTemp = (LinkList)malloc(sizeof(Node));
if (!pTemp)
{
printf("malloc error!\n");
return -1;
}
pTemp->data = e;
head = pList; //让head指向链表pList的头结点
//头结点后插入结点
pTemp->next = head->next;
head->next = pTemp;
return TRUE;
}
尾部插入,将数据元素插入到尾节点后面,这种简称为尾插法。
/******************************************
名称: 尾部后插入元素操作
功能: 在链表尾部后插入存储数据元素的结点
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsertTail(LinkList pList, const ElemType e)
{
LinkList cur, pTemp;
//检测链表是否存在
if (!pList)
{
printf("pList not exist!\n");
return FALSE;
}
pTemp = (LinkList)malloc(sizeof(Node));
if (!pTemp)
{
printf("malloc error!\n");
return -1;
}
pTemp->data = e;
cur = pList; //让head指向链表pList的头结点
//找到链表尾节点
while (cur->next)
{
cur = cur->next;
}
//尾结点后插入结点
pTemp->next = cur->next;
cur->next = pTemp;
return TRUE;
}
6 单链表的整表删除
单链表整表删除的算法思路如下:
- 声明一结点p和q;
- 将第一个结点赋值给p;
- 循环:
- 将下一结点赋值给q;
- 释放p;
- 将q赋值给p。
实现代码算法如下:
/******************************************
名称: 整表删除操作
功能: 销毁整个线性表,即释放所有结点申请的内存
返回值: 删除成功返回true,否则返回false
******************************************/
Status ClearList(LinkList pList)
{
LinkList p; //当前结点
LinkList q; //用来保存下一结点,防止释放当前结点后导致“掉链”
//检测链表是否存在
if (!pList)
{
printf("pList not exist!\n");
return FALSE;
}
p = pList; //q指向头结点
while (p)
{
q = p->next; //事先保存下一结点,防止释放当前结点后导致“掉链”
free(p); //释放当前结点
p = q; //将下一结点赋给当前结点p
}
pList->next = NULL; //头结点指针域指向空
return TRUE;
}
7 单链表的完整实现
#include "stdafx.h"
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
typedef int Status; //Status是函数结果状态,成功返回TRUE,失败返回FALSE
typedef int ElemType;
/* 线性表的单链表存储结构 */
typedef struct node
{
ElemType data;
struct node *next;
}Node;
typedef Node* LinkList; //定义单链表指针
/******************************************
名称: 初始化单链表操作
******************************************/
void InitList(LinkList *pList) //必须使用双重指针,一重指针申请会出错
{
*pList = (LinkList)malloc(sizeof(Node));
(*pList)->data = 0;
(*pList)->next = NULL;
}
/******************************************
名称: 获取元素操作
功能: 获取链表第i个结点数据,赋给e
返回值: 查找成功返回true,否则返回false
******************************************/
Status GetElem(LinkList pList, int i, ElemType *e)
{
LinkList cur;
cur = pList->next; //让cur指向链表pList的第1个结点
int j = 1; //j为计数器,赋值为1,对应cur指向结点
//cur不为空或者计数器j还没有等于i时,循环继续
while (cur != NULL && j < i) //若i=1,则不用进入循环遍历,就可找到了
{
cur = cur->next;
j++;
}
if (cur == NULL || j>i) //j>i,为了避免输入i小于1的情况
return FALSE; //到链表尾,仍未找到第i个元素
*e = cur->data; //取第i个元素的数据
return TRUE;
}
/******************************************
名称: 插入元素操作
功能: 在链表第i个位置插入新的数据元素e
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsert(LinkList pList, int i, const ElemType e)
{
LinkList front; //指向插入位置前一个结点
LinkList pTemp;
int j;
front = pList; //让front指向链表pList的头结点
j = 1; //j为计数器,赋值为1,让front能指向插入位置前一个结点
//找到i位置所在的结点
while (front != NULL && j < i)
{
front = front->next;
j++;
}
if (front == NULL || j>i) //j>i,为了避免输入i小于1的情况
return FALSE; //到链表尾,仍未找到第i个元素
pTemp = (LinkList)malloc(sizeof(Node));
pTemp->data = e;
//插入结点s
pTemp->next = front->next;
front->next = pTemp;
return TRUE;
}
/******************************************
名称: 删除元素操作
功能: 在链表第i个位置删除结点
返回值: 删除成功返回true,否则返回false
******************************************/
Status ListDelete(LinkList pList, int i, ElemType *e)
{
LinkList front, pTemp;
front = pList; //让front指向链表pList的头结点
int j = 1; //j为计数器,赋值为1,让front能指向插入位置前一个结点
//找到i位置所在的结点
while (front != NULL && j < i)
{
front = front->next;
j++;
}
if (front == NULL || j > i) //j>i,为了避免输入i小于1的情况
return FALSE; //到链表尾,仍未找到第i个元素
LinkList q = front->next;
//删除结点
front->next = front->next->next;
//使用q结点,保存已经删除的结点
*e = q->data; //将要删除结点的数据赋给e
free(q);
q = NULL;
return TRUE;
}
/******************************************
名称: 头部后插入元素操作
功能: 在链表头部后插入存储数据元素的结点
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsertHead(LinkList pList, const ElemType e)
{
LinkList head, pTemp;
//检测链表是否存在
if (!pList)
{
printf("pList not exist!\n");
return FALSE;
}
pTemp = (LinkList)malloc(sizeof(Node));
if (!pTemp)
{
printf("malloc error!\n");
return -1;
}
pTemp->data = e;
head = pList; //让head指向链表pList的头结点
//头结点后插入结点
pTemp->next = head->next;
head->next = pTemp;
return TRUE;
}
/******************************************
名称: 尾部后插入元素操作
功能: 在链表尾部后插入存储数据元素的结点
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsertTail(LinkList pList, const ElemType e)
{
LinkList cur, pTemp;
//检测链表是否存在
if (!pList)
{
printf("pList not exist!\n");
return FALSE;
}
pTemp = (LinkList)malloc(sizeof(Node));
if (!pTemp)
{
printf("malloc error!\n");
return -1;
}
pTemp->data = e;
cur = pList; //让head指向链表pList的头结点
//找到链表尾节点
while (cur->next)
{
cur = cur->next;
}
//尾结点后插入结点
pTemp->next = cur->next;
cur->next = pTemp;
return TRUE;
}
/******************************************
名称: 整表删除操作
功能: 销毁整个线性表,即释放所有结点申请的内存
返回值: 删除成功返回true,否则返回false
******************************************/
Status ClearList(LinkList pList)
{
LinkList p; //当前结点
LinkList q; //用来保存下一结点,防止释放当前结点后导致“掉链”
//检测链表是否存在
if (!pList)
{
printf("pList not exist!\n");
return FALSE;
}
p = pList; //q指向头结点
while (p)
{
q = p->next; //事先保存下一结点,防止释放当前结点后导致“掉链”
free(p); //释放当前结点
p = q; //将下一结点赋给当前结点p
}
pList->next = NULL; //头结点指针域指向空
return TRUE;
}
/******************************************
名称: 遍历链表并显示元素操作
******************************************/
void ListShow(LinkList pList)
{
LinkList cur = pList->next;
while (cur != NULL)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
int main()
{
LinkList pList;
//初始化单链表
InitList(&pList);
//插入结点
ListInsert(pList, 1, 0);
ListInsert(pList, 2, 1);
ListInsert(pList, 3, 2);
//删除结点
int val;
ListDelete(pList, 2, &val);
printf("删除结点的数据: %d\n", val);
//头部后插入元素
ListInsertHead(pList, 5);
ListInsertHead(pList, 4);
//尾部后插入元素
ListInsertTail(pList, 8);
ListInsertTail(pList, 9);
//遍历链表并显示元素操作
ListShow(pList);
//删除整个单链表
ClearList(pList);
return 0;
}
/*
输出结果:
删除结点的数据: 1
4 5 0 2 8 9
*/