单链表-线性表的链式存储结构
链式存储结构的特点
顺序存储结构里面,每次都要预先分配一片连续的内存,有些时候分配的空间会有剩余,有些时候分配的空间又不足,灵活性较差,因此,为了解决这个问题,诞生了链式存储结构。那么,链式存储结构有些什么样的特点呢?
存储位置任意:链式存储结构用一组任意存储单元来存放线性表的数据元素,这组存储单元可以在内存中任意未被占用的位置。
可以和顺序存储结构做对比,链式存储结构不需要提前分配一片连续的内存,每个数据元素中除了要存储的数据信息,还得有一个存储后继元素地址的存储单元(一般用指针来实现)
单链表
- 链式存储结构中的一些通用叫法
- 数据域:存储数据元素信息的域
- 指针域:存储后继元素位置的域
- 指针/链:在指针域中存储的信息
- 结点(Node):数据域和指针域组成的数据元素
n个结点链接成一个链表,即为线性表(a1,a2,a3,…,an)的链式存储结构,由于这个链表里面每个结点只包含一个指针域,所以叫单链表。
头结点、头指针
首先,头结点的数据域不存储任何信息,那么,头结点和头指针有什么异同呢
-
头指针
- 头指针是链表指向第一个结点的指针,如果链表中有头结点,那么头结点则是指向头结点的指针
- 头指针有标识作用,常用头指针(其指针变量名)冠以链表名字
- 无论链表是否为空,头指针均不为空
- 头指针是链表的必要元素
-
头结点
- 头结点是为了操作的统一和方便而设立的,放于第一个元素节点前,它的数据域一般没有什么意义(可以用来存放链表长度)
- 有了头结点,对在第一个元素结点前插入结点和删除第一个结点的操作就和其他结点的操作统一了
- 头结点并非是链表的必须要素
**简而言之,头指针指向第一个结点(包括头结点),是链表的必要元素,头结点是为了操作的方便加上的,非链表的必要元素。**如下图:
单链表存储结构
typedef struct Node //结点
{
ElemType data; //数据域
struct Node *next; //指针域
}Node;
typedef struct Node* LinkList; //头指针
假设 p 指向线性表第i个元素指针,则该结点 ai 数据域我们可以用 p->data 的值来代表其数据元素,ai 的结点域我们可以用 p->next 来表示,其中 p->next 的值是一个指针。
p->data == ai
p->next->data == ai+1
单链表创建,读取,插入,删除的简单思路
创建
我们知道顺序结构线性表的创建是利用了数组的初始化,它的占用内存是不变的;单链表则不同,它的内存增长为动态的过程。所以创建单链表的过程就是一个动态生成链表的过程,从空表的状态起,一次建立各元素结点并且逐个插入链表。
整表创建的算法思路:
- 声明结点p和计数器变量i
- 初始化空链表L
- 让L的头结点指针指向NULL,即建立一个带头结点的单链表
- 循环实现后继结点的赋值和插入
- 头插法:从空表开始,生成新结点,读取数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,知道结束为止。简单说就是把新加进的元素放在表头后第一个位置(让新结点的next指向头结点的next后,让表头的next指向新结点),但是生成链表中结点的次序和输入顺序相反
- 尾插法:用头结点指向新结点,移动指针,用上一个结点指向新结点,循环结束后使尾结点指向NULL
读取
单链表的读取就是循环移动指针找到结点之后读取
插入
单链表的插入就是循环移动指针找到插入位置上一个结点之后,生成一个新结点,给新结点赋值,使新结点指向原位置结点之后再使上一个结点指向新结点
删除
- 删除一个数据元素:找到要删除的上一个位置,定义一个临时指针变量,指向删除位置,使上一个位置指针指向临时指针变量指向的下一个元素,释放临时变量指向的结点内存
- 整表删除:用两个(Node*)类型的指针指向第一个数据元素,其中一个指针比另一个移动慢一次,释放慢的那个指针指向的结点占用内存,最后将头结点指向NULL
单链表常见操作代码示例
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int status; //函数状态量
typedef int ElemType; //元素数据类型,根据情况而定,这里是int类型
typedef struct Node //申明结点
{
ElemType data;
struct Node* next;
}Node, * LinkList;
void CreateListHead(LinkList* L, int n); //头插法创建单链表
void CreateListTail(LinkList* L, int n); //尾插法创建单链表
status GetElem(LinkList L, int i, ElemType* e); //单链表的读取
status ListInsert(LinkList* L, int i, ElemType e); //单链表的插入操作
status ListDelete(LinkList* L, int i, ElemType* e); //单链表的删除
status ClearList(LinkList* L); //单链表的整表删除
void printList(LinkList L); //打印单链表
int main(void)
{
printf("\n===============请选择你要进行的操作=================\n");
printf(" 1 : 头插法创建单链表 |\n");
printf(" 2 : 尾插法创建单链表 |\n");
printf(" 0 : 退出程序 |\n");
printf("******************************************************\n");
while (1)
{
int select1;
LinkList L = (LinkList)malloc(sizeof(Node)); //申请一个头结点
AAA: scanf("%d", &select1);
getchar();
switch (select1)
{
case 1:
{
int n;
printf("请输入你要建立线性表元素的个数:");
scanf("%d", &n);
CreateListHead(&L, n);
break;
}
case 2:
{
int n;
printf("请输入你要建立线性表元素的个数:");
scanf("%d", &n);
CreateListTail(&L, n);
break;
}
case 0:
{
exit(0); break;
}
default:
{
printf("输入错误,请重新输入");
goto AAA;
}
}
printf("下面是你创建的线性表\n");
printList(L);
putchar('\n');
while (1)
{
int select2;
printf("\n=============请继续选择你要进行的操作===============\n");
printf(" 1 : 查找一个元素 |\n");
printf(" 2 : 插入一个元素 |\n");
printf(" 3 : 删除一个元素 |\n");
printf(" 0 : 退出对线性表操作 |\n");
printf("******************************************************\n");
printf("\n请选择:");
CCC: scanf("%d", &select2);
getchar();
switch (select2)
{
case 1:
{
int i, e=0;
printf("请输入你要查找数据的位置:");
scanf("%d", &i);
getchar();
GetElem( L, i, &e);
printf("你要查找的数据为%d:\n", e);
break;
}
case 3:
{
int i, e = 0;
printf("请输入你要删除数据的位置:");
scanf("%d", &i);
getchar();
ListDelete(&L, i, &e);
printf("你要删除的数据为%d:\n", e);
break;
}
case 2:
{
int i, e;
printf("请输入你要在哪个位置前插入数据:");
scanf("%d", &i);
getchar();
printf("请输入你要插入的数字:");
scanf("%d", &e);
ListInsert(&L, i, e);
break;
}
case 0:
{
goto BBB;
}
default:
{
printf("输入错误,请重新输入");
goto CCC;
}
}
printf("一顿操作后线性表变为:\n");
printList(L);
putchar('\n');
}
BBB: ClearList(&L);
}
return 0;
}
/*单链表的创建*/
/*头插法创建单链表,就是在空表基础上,每次创建一个新的结点,先将新节点链到上一个结点,然后将头结点指向新节点*/
void CreateListHead(LinkList* L, int n)
{
Node* s;
/*生成随机数种子*/
time_t ts;
srand((unsigned int)time(&ts));
(*L) = (LinkList)malloc(sizeof(Node)); //申请一片内存存放头结点;
(*L)->next = NULL;
for (int i = 1; i <= n; i++)
{
s = (Node*)malloc(sizeof(Node));
s->data = rand() % 100 + 1; //随机生成1-100的数赋值给每个数据元素
s->next = (*L)->next;
(*L)->next = s;
}
}
/*尾插法创建单链表,就是在一个头结点后面,将第一个结点指向新结点,然后将上一个结点向后移动一位,依次循环,最后将尾结点指向NULL*/
void CreateListTail(LinkList* L, int n)
{
Node* s;
LinkList p;
/*生成随机数种子*/
time_t ts;
srand((unsigned int)time(&ts));
(*L) = (LinkList)malloc(sizeof(Node)); //申请一片内存存放头结点;
p = (*L);
for (int i = 1; i <= n; i++)
{
s = (Node*)malloc(sizeof(Node));
s->data = rand() % 100 + 1;
p->next = s;
p = p->next;
}
p->next = NULL;
}
/*单链表的读取,读取就是从头结点开始依次循环,最后找到那个位置元素*/
status GetElem(LinkList L, int i, ElemType* e)
{
if (i < 1) //单链表是从1开始计数的,0位置无可用数据元素
{
return ERROR;
}
LinkList p = L->next;
for (int n = 1; n < i; n++)
{
if (p == NULL) //如果链表循环到尾结点,还没有到i,则结束循环,返回错误
{
return FALSE;
}
p = p->next; //依次将指针后移
}
*e = p->data; //返回数据元素的值给e
return OK;
}
/*单链表的插入操作,就是循环移动指针到插入位置,建立一个新结点,使它指向后一个结点,再使之前前一个结点指向新结点*/
status ListInsert(LinkList* L, int i, ElemType e)
{
if (i < 1)
{
return ERROR;
}
LinkList p = (*L)->next;
int j = 1; //计数器,记录指针每次指向第几个结点
while (p && j < i - 1) //移动p使p指向第i个数据元素的前一个数据元素,p为空时或者j<i-1循环结束
{
p = p->next;
j++;
}
while (!p || j > i - 1) //如果p->next为空或者j>i-1,则返回错误
{
return ERROR;
}
Node* s = (Node*)malloc(sizeof(Node)); //生成一个新结点
s->data = e; //将e的值赋值给结点数据域
/*插入*/
s->next = p->next;
p->next = s;
return OK;
}
/*单链表的删除,就是循环到要删除位置的上一个位置,使那个元素指向下两个元素,释放要删除元素的内存*/
status ListDelete(LinkList* L, int i, ElemType* e)
{
if (i < 1)
{
return ERROR;
}
LinkList p = (*L)->next;
int j = 1; //计数器,记录指针每次指向第几个结点
while (p && j < i - 1) //移动p使p指向第i个数据元素的前一个数据元素,p为空时或者j<i-1循环结束
{
p = p->next;
j++;
}
while (!(p->next) || j > i - 1) //如果p为空或者j>i-1,则返回错误
{
return ERROR;
}
LinkList temp = p->next; //临时变量存储p->next的值
*e = temp->data;
p->next = temp->next; //删除
free(temp); //释放内存
return OK;
}
/*单链表的整表删除,就是用两个(Node*)类型的指针指向第一个数据元素,其中一个指针比另一个移动慢一次,释放慢的那个指针指向的结点占用内存,最后将头结点指向NULL*/
status ClearList(LinkList* L)
{
Node *p, *q;
p = (*L)->next;
while (p)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}
void printList(LinkList L)
{
if (L->next == NULL)
{
printf("\n单链表是空表\n");
}
LinkList p = L->next;
int j = 1;
while (p)
{
printf("[%d]%-6d", j, p->data);
j++;
p = p->next;
}
}
下面是运行示例
由上面的代码可以看出来,单链表的插入和删除时间复杂度都为O(n),对比顺序结构无太大优势;但是,存在即合理,例如:若从第i个位置开始,插入连续10个元素,顺序存储结构每次插入就都需要移动n-i个位置,但是单链表只需要第一次找到第i个位置的指针后每次移动指针,时间复杂度都是O(1)。
所以,对于插入或者删除数据越频繁的操作,单链表效率越高
单链表对比顺序表优缺点
存储分配方式
顺序存储一般用一段连续的存储单元依次存储线性表的数据元素
单链表一般用链式存储结构,用一组任意存储单元存放线性表的元素
时间性能
-
查找
- 顺序表O(1)
- 单链表O(n)
-
插入和删除
- 需要平均移动表长一半的元素,时间为O(n)
- 单链表计算出位置指针后,插入和删除时间为O(1)
-
空间性能
- 顺序表需要预分配存储空间,分大了浪费,分小了又容易溢出
- 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制
结论
若线性表需要频繁查找,很少进行插入和删除操作,宜采用顺序存储结构,反之则采用单链表结构;预先知道线性表长度,则采用顺序表,反之采用单链表