本系列博客是博主在学习数据结构时的笔记,希望与大家一起分享,如有对数据结构感兴趣的小伙伴可以加个联系方式一起交流。本系列参考程杰的《大话数据结构》
线性表就是零个或者多个数据元素的有限序列。线性表有两组存储方式:顺序结构存储和链式结构存储
1、顺序结构存储
引言:可以想象一群学生在教室占座,比如有9个人,那么就要占9个位置,因为预先已经知道了人的数目。但是当某同学事先去占位后,发现有3位同学没来,但是已经用书本帮他们占座了,其它同学也就没坐在这个位子上了,那么也就意味了这三个空位也浪费了。假如就来了这6位同学,那么该线性表的长度是多少呐?答案当然是6。虽然预先已经开辟了9个单元的空间了,但是线性表的长度是指具有元素的个数,不过该数组的长度依然还是9,这一点需要注意。然后上课上到一半,这3位同学过来了,还带了另外寝室的4位同学,此时总共只占了9个位置,仅能让这3位同学坐下,另外4位自然是坐不下的,也就是线性表的当前长度不能超过数组的长度。
1.1读取
根据下标即可读取,此处省略。
1.2 插入
假设要在数组A = [1,2,4,5]的第i位下标(注意下标从0开始)插入3。首先找到第i位,把第i位及其后面的数据全部后移一位,线性表长度加1。注意在实现数据右移的过程中切记要从后往前实现赋值,否则造成数据的覆盖。
//插入
status ListInsert(SqList *L,int i,Elmtype e)
{
if(L->length == MAXSIZE)//线性表预设最大长度
return ERROR;
if(i<0 || i>L->length-1)
return ERROR;
if(i<L->length)
{
for(int k = L->length-1;k > i;k--)
{
L->data[k] = L->data[k-1];//一直向后赋值,直到第i+1位得到了第i位的值
}
}
L->data[i] = e;
L-length++;
return OK;
}
1.3 删除
比如在第2下标处插入3,现在的数组A = [1,2,3,4,5],现在想把第1下标的元素删除。删除的时候需要将后继元素向前推移,链表长度减1即可。
//删除
status ListInsert(SqList *L,int i,Elmtype e)
{
if(L->length == MAXSIZE)//线性表预设最大长度
return ERROR;
if(i<0 || i>L->length-1)
return ERROR;
if(i<L->length)
{
for(int k = i; k < L->length-1; k++)
{
L->data[k] = L->data[k+1];//一直向前赋值
}
}
L-length--;
return OK;
}
1.4 优缺点
顺序存储结构读取数据的时间复杂度位O(1),插入和删除数据的时间复杂度为O(n)。因此优点在于可以快速的存取表中的任意位置的元素,此外无需为表中元素间的逻辑关系而增加额外的存储空间。缺点也很明显,首先是顺序存储的缺点,造成存储空间的碎片化;其次是由于需要实现确定好容量,因此当线性表长度变化较大时难以为继;第三点就是插入和删除数据的时间复杂度很大。
2、链式结构存储
链式结构的特点是哪里有空位我就往哪里连,这样无需开辟一块连续的空间,合理的利用存储空间。既然是哪里有空位就连哪里,不像顺序存储,前后的位置都是相连的,很容易可以找到前面和后面的人,链式这种跳跃性的存储方式怎么实现呐?答案很简单,前一个人只要知道他后面这个人的家庭住址不就行了嘛!也就是地址,实际是指向该元素的指针。那么这个人除了要记住自己是谁(也就是自己存储的内容),还要知道下一个人的家庭住址(指针)。由这两部分信息组成的数据元素(这里我们称为a_i)的存储映像称为结点(Node)。
对于线性表来说,链表的第一个结点的存储位置叫做头指针,其实也就是头指针指向线性表的第一个结点。尾结点指向为空,因此设置为NULL。
有时候为了方便对链表进行操作,会在链表的第一个结点前再设置一个头结点,头结点的指针域存储指向第一个结点的指针,如下图所示。头结点的数据域一般存储线性表长度等一些附加信息。
有同学可能会疑问,头指针和头结点有啥区别啊?这里面区别大着呐,千万不能混淆这两个概念。首先链表必须要有头指针,没有头指针无法知道链表从哪开始,即使链表是空链表也必须要知道头指针。头指针是指向链表的第一个结点的指针,如果该链表有头结点,那就是指向头结点的指针,比如上面的头结点是地址是1000,那么头指针中存储的就是1000。而头结点只是为了方便操作才设立的。
下面是单链表的存储结构描述:
//结点定义
typedef struct Node
{
Elmtype data;//数据
struct Node *next;//指针
}Node;
typedef struct Node *LinkList;//给结构体Node另起个名字LinkList
假设指针p是指向a_i的指针,那么a_i的数据为p->data,ai的指针域为p->next,其中p->next又指向a_i+1元素,a_i+1的数据域为p->next.data。如下图所示:
2.1读取
单链表的读取的时间复杂度取决于i的位置,最坏是O(n),由于链表无法直接获取下标进行读取,只能通过遍历的方式。
//读取
status GetElem(LinkList L,int i,Elmtype e)
{
LinkList p;//定义一个结点
p = L->next;//让p指向链表的第一个结点
int j = 1;//定义一个计数器,已经指向第一个结点了,因此从1开始
while(p && j<i)//p不为空以及还没有遍历到i执行循环体
{
p = p->next;
j++;
}//获取第i个结点
if(!p || j>i)r//如果第i个结点不存在或者j>i
{
return ERROR;
}
e = p->data;//获取数据
return OK;
}
博主在做这种题的时候总是搞不清循环判断条件,到底是到第i位还是第i-1位,主要总是将下标从0和从1开始弄混。这里的第i个结点是从第一个结点开始的,链表哪有什么第0个结点呐。上面的p开始指向第一个结点,循环i-1次之后,不就是指向第i个结点了嘛,这样理解就简单多了。
2.2插入
假设结点s要插到p和p的下一个结点之间,那么应该怎么做呐?
实际上很简单,把p和p->next的关系打破,再手牵手s不就行了。
也就是s ->next = p ->next;p ->next = s;s ->data = e;切记不能先让p ->next = s,这样的话p指向的结点会被s给覆盖。
代码描述如下:
//在第i个结点前插入
status ListInsert(LinkList *L,int i,Elmtype e)
{
LinkList p,s;
p = *L;
s = (LinkList)malloc(sizeof(Node));
int j = 1;
while(p && j < i)
{
p = p->next;
j++;
}//找到第i-1个结点
if(!p || j>i)
return ERROR;
s->next = p->next;
p->next = s;
s->data = e;
return OK;
}
2.3删除
删除操作比较简单,直接把删除元素前一个结点的指向变为该元素的下一个结点即可。
假设删除第i个结点,首先要找到第i-1个结点记为p,则该结点为p->next,这里为了方便起见我们记为q,q->next指向q的下一个结点也就是p->next->next。那么删除q结点只需要p->next = q->next;然后再释放q即可。
代码描述如下:
//删除第i个结点
status ListDelete(LinkList *L,int i,Elmtype e)
{
LinkList p,s;
p = *L;
s = (LinkList)malloc(sizeof(Node));
int j = 1;
while(p && j < i)
{
p = p->next;
j++;
}//找到第i-1个结点
if(!p || j>i)
return ERROR;
q = p-> next;
p->next = q->next;
e = q.data;
free(q);//收回q的内存
return OK;
}
2.4单链表的整表创建
头插法创建整表思路:
1、首先需要初始化空链表L,该链表具有头结点,头结点的指针指向NULL;声明一个结点指针p。
2、往空链表里面插数据,首先生成一个新结点赋值给p,然后随机生成一系列数字给p->data,最好在头结点和前一新结点之间插入p。
空链表如下:
图解析如下:
代码描述如下:
//头插法创建带头结点的链表
void CreatListHead(LinkList *L,int n)
{
L= (LinkList)malloc(sizeof(Node));
L->next = NULL;
LinkList p;//p的初始化应该在插入结点的时候
//初始化随机数种子
srand(time(0));
//插入新结点
for(int i=0;i<n;i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;//生成100以内的数字
p->next = L->next;
L->next = p;
}
}
每次加入一个新结点都要往头结点和前一新结点之间插,有点插队的意思。事实上我们可以遵循排队的思想,新来的结点要守规矩,接在上一个结点的后面,这种算法称为尾插法。
尾插法创建整表思路:
如下图新结点直接插在尾结点r(定义r指向L的尾部)的后面,即p = r->next;然后很显然的p成为了尾结点,则r = p;最后在链表结束时,需要尾结点指向NULL(切记!!!)。
代码描述如下:
//尾插法创建带头结点的链表
void CreatListTail(LinkList *L,int n)
{
L= (LinkList)malloc(sizeof(Node));
LinkList p,r;
r = L;//定义r指向L的尾部
//初始化随机数种子
srand(time(0));
//插入新结点
for(int i=0;i<n;i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;//生成100以内的数字
r-> next = p;
r = p;
}
r->next = NULL;//当前链表结束
}
2.5单链表的整表删除
整表删除需要一个结点一个结点做个循环释放内存即可,但是在释放内存的时候,先要保存当前要释放的结点的指向,不然释放完这个结点后无法找到下一个结点的地址。
在这里需要定义两个结点p和q,q充当临时保存的功能,p释放。
代码描述如下:
//整表删除
status ClearList(LinkList *L)
{
LinkList p,q;
p = L->next;//p指向第一个结点
while(p)//没到表尾NULL
{
q = p->next;//保存下一结点位置
free(p);
p = q;
}
L->next = NULL;//头结点的指针域为空
return OK;
}
以上是顺序表和链表的全部基本内容,如有不对之处请于评论处指正,谢谢!