顺序存储结构里面,每次都要预先分配一片连续的内存,有些时候分配的空间会有剩余,有些时候分配的空间又不足,灵活性较差,因此,为了解决这个问题,诞生了链式存储结构。那么,链式存储结构有些什么样的特点呢?
存储位置任意:链式存储结构用一组任意存储单元来存放线性表的数据元素,这组存储单元可以在内存中任意未被占用的位置。
可以和顺序存储结构做对比,链式存储结构不需要提前分配一片连续的内存,每个数据元素中除了要存储的数据信息,还得有一个存储后继元素地址的存储单元(一般用指针来实现)
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
我们知道顺序结构线性表的创建是利用了数组的初始化,它的占用内存是不变的;单链表则不同,它的内存增长为动态的过程。所以创建单链表的过程就是一个动态生成链表的过程,从空表的状态起,一次建立各元素结点并且逐个插入链表。
整表创建的算法思路:
单链表的读取就是循环移动指针找到结点之后读取
单链表的插入就是循环移动指针找到插入位置上一个结点之后,生成一个新结点,给新结点赋值,使新结点指向原位置结点之后再使上一个结点指向新结点
#include
#include
#include
#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
{
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
{
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)。
所以,对于插入或者删除数据越频繁的操作,单链表效率越高
顺序存储一般用一段连续的存储单元依次存储线性表的数据元素
单链表一般用链式存储结构,用一组任意存储单元存放线性表的元素
查找
插入和删除
空间性能
若线性表需要频繁查找,很少进行插入和删除操作,宜采用顺序存储结构,反之则采用单链表结构;预先知道线性表长度,则采用顺序表,反之采用单链表