首先申明,文章的所有代码正确率可以保证,但是需要你自己去逐行分析,因为这样可以提升读者的代码理解能力对结构有更加清晰的认识同时作者在写作时有一些函数的大小写没有区分这里需要注意一下,可以放进编译器里面去纠错。
很多同学在学习数据结构这门课时后期常常会感到很痛苦,这是因为没打好基础导致后面的房屋越建越斜。而线性表就是整个数据结构的基础,为什么这么说呢?线性表可以带盖概括为两部分,顺序表和链表,在后期的学习中很多结构都会用到链表为基础,那么你的链表这里都似懂非懂请问你还如何更好的进行后期的学习呢?
接下来我将进行线性表这一章节的讲解,本节参考了严蔚敏老师的数据结构以及王道25考研数据结构这一本书。好了接下来就是讲解
线性表这一部分大概可以分为两大板块
线性表是一种逻辑结构而接下来我们需要探讨的是数据的物理结构这里需要注意一下。
1.顺序表
2.链表
第一部分 顺序表
1.顺序表
见名知意,顺序表就是顺序存储的表。
顺序表特点:
1.顺序存储
2.申请的地址空间必须联系
3.个数有限且类型相同
4.除表头都有直接前驱,除表尾都有直接后继。
顺序表需要掌握的知识点基本上就上面这些接下来是对顺序表的一些操作进行讲解。
首先给出一个存储结构的表示
#define maxsize 30
typedef struct{
elemtype data [maxsize] ;//数据类型➕最大长度的定义
int lenvth;//表长的定义
}sqlist
//为什么这里maxsize,define maxsize 30与elemtype data [maxsize]
你可以看成用数组的方式来申请空间,elemtype data [maxsize]表示要申请elemtype类型且长度为maxsize的地址空间,每一个空间的大小为sizeof(elemtype),总的地址空间为sizeof(elemtype)*maxsize
//静态的初始化操作
void initlist(sqlist &L){
L.length=0; //为什么要用&,这里你可以简单记一下所有需要带返回的东西引用的时候都需加
//&符号,如果你不加只是将原来的东西复制了一份进行操作而已,但需要执行果
//是说用的值仍然是原来未进行操作过的
}
//插入这里我们的返回值类型设为bool型若是成功返回true失败返回fales
//一般是在第i个位置插入新的元素
bool listinsert (sqlist &L,int i,elemtype e)//elemtype在这里代表任何你想用的任何数据类型
{
if(i<1||i>L.length+1)//length+1可以理解为我们可以在表尾的位置插入如表长10,我在第11个位置插入
return false;//这里是对i的值进行合法检测
if(L.length>=MAXSIZE)//判满
return false
for(int j=L.length;j>i;j--)
L.data[j]=L.data[j-1];
L.data[i-1]=e;//第i个位置对应的下标是i-1>。
L.length++;
return true;
}
//删除,这里也采用布尔类型返回是否成功的消息
bool listdelete(sqlist &L int i ,elemtype &e)//这里e用了&符号用来返回删除的值
{
if(i<1||i>L.length)
return false;//判断值是否合法
if(L.length=0)
return fslse;
e=L.data(i-1);
for(int j=i;j<L.length;j++)
L.data(j-1)=L.data(j)//第i个位置存储的是第i+1个元素
L.length--;
return true;
}
//查找(按值)//直接遍历整个数组
bool locateelem(sqlist &L ,elemtype e)
{
int i;
for(i=0,i<L.length;i++)//从0开始到等于length结束恰好是length-1+1次
if(L.data(i)==e)
return i//这里返回的是下标
eles
return false
}
//按照序号查找
bool locateelem(sqlist &L ,int i , int &e)
{
if(i<1||i>L.length-1)
return false;
e=L.data(0+e);
return ture;
}
//以上是静态分配的定义
//接下来我会给出动态分配的定义,这里动态分配解决了一个问题,就是顺序表在进行分配地址空间时需要一次性分配结束后期不可更改(这句话针对所有顺序表),那为什么又说可以动态分配呢。其实这里是做了一个小小的迁移,你们可以理解为手机搬家扩容这种形式
//动态分配存储结构
#define maxsize 100
typedef struct{
elemdata *data;
int maxsize ,length;
}seqlist
//初始化
void initlist(seqlist &L){
L.data=(elemtype *)malloc(initsize*sizeof(elemtype));//分配空间
L.length=0;
L.maxsize=initsize;
}
//动态增加数组的长度
void IncreaseSize(Seqlist &L,int len){
int *p=L.data;
L.data=(int *)(malloc(sizeof(int)*(L.MaxSize+len));
for(int i=0;i<L.length;i++){
L.data[i]=p[i]; //将数据复制到新的区域
}
L.MaxSize=L.MaxSize+len;
free(p); //释放原来的内存空间
}
//动态分配的增删改查操作仿造静态分配的增加的话特殊一点先判断值是否合法,同时判断容量满没有如果满了先扩容一下再进行插入
以上呢就是有关顺序表需要掌握的知识点了
对了这里补充一下各种操作的复杂度(以静态分配为例)
增平均为O(n)
随机访问O(1)
删除O(n)
顺序表的随机访问特性很好,但是他的插入和删除操作平均都需要移动一般的元素,这是一个最致命的缺点,但是这也是链表的一个优点。后续我们会详细的讲解链表。
2.链表
对于链表呢我们需要熟悉各种操作,前面也说过采用链式存储是一个非常常规的操作,除非你去更改操作系统底层代码,不然对于百分之80的应用场景来说链式存储都是最优解也是操作系统默认的做法。接下来我们进行链表的讲解。
单链表,单链表的一个数据元素由两个数据项组成分别是数据域和指针域其中指针域是指向下一个元素的指针。
接下来来区分几个知识点,
头节点,带头节点的第一个节点,一般不存储什么数据当然你想也可以存但是默认不让,有头节点的时候说明一定是一个带头节点的链表且链表的第一个元素一定是头节点的next指针所指的元素。
头指针,指向链表的第一个元素,这里分一下带头节点和不到头节点的链表,带头节点头指针指向头节点但是头节点并不是第一个数据元素,不到头节点的链表头指针指向链表的第一个元素。
为什么会有头节点这样复杂的东西?因为引入头节点以后可以让链表的第一个元素的操作和其他元素保持一致,避免再进行操作时还需要单独考虑第一个元素带来的额外操作。
接下来是单链表的一些基本操作,和顺序表一样也是增删改查等操作。大家一定要领悟操作的精髓步骤,领悟其中的核心代码。
//单链表的数据结构
typedef struct LNode{
elemtype data;//数据域
struct LNode*next;//指针域
}LNode,*Linklist
//初始化
bool initlist(linklist &L)
{
L=(LNode*)malloc(size of(LNode));//创建头节点
L->next=null;//头节点后没有元素节点
return true;
}
//求表长
int length(linklist &L ){
int length=0;
L.Node *p=L;
whlie(p->next!= null)
p=p->next;
length++;
return length;
}
//按序号查找结点,给出值
LNode *getelem(linklist L,int i)
{
LNode *p =L;
int j=0
while(j<i&&p!=null)//为什么是j<i呢?因为从0到i-1恰好是i次遍历。为什么!=null防止溢出表的查找
p=p->next;
j++
return p;
}
//按值查找,给出序号
LNode *locateelem(linklist L,elemtype e){
LNode *p =L;
while(p->next!=null&&p->next->data!=e)
p=p->next;
return *(p+1);
}
//插入的操作
bool listinsert (linklist &L ,int i ,elemtype e)
{
Lnode *p=L;
//接下来这个找到i-1个节点操作可以进行封装后面会用到很多次,所谓的封装你可以理解为将它形容成一个函数来调用
int j=0
while(j<i-1i&&p!=null)//从0到i-1恰好遍历了i-1次
p=p->next;
j++;
//经过这一段代码以后*p就是指向
if(p==null)
return false;
//将新的节点接入到第i-1个节点的后面就完成了第i个位置的插入
//先申请一个新的同类型节点存放新数据
LNode *s=(LNode*)malloc(size of(LNode));//这里为什么不是elemtype因为顺序表只需要malloc一个
//data类型,但是链表还需要有指针域,所以直接用LNode来
//进行地址空间的申请
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
//对于插入操作我们还可以进行一种偷换概念的做法
s->next=p->next;
p->next=s;
temp=p->data;
p->data=s->data;
s->data=temp;
//这里是这样的先将s插入到p的后面然后互换sp的数据域部分
//删除节点,要删除第i个节点需要找到第i-1个节点将他的指针域指向i+1然后free(i)
bool listdelete(linklist &L,int i,elemtype e){
LNode *p=L;//可以理解为L是头节点
//接下来的代码就是前面说过的可以进行封装的那一串找i-1个节点的代码
int j=0;
while(j<i-1&&p!=null)
p=p->next;
j++;
//以上代码找到了第i-1个节点
if(p==null||p->next==null) //i-1和i都不能为空
return false;
LNode *q=p->next;
e=q->data;
p->next=q->next;
free(q);
return true;
}
//单链表的建立,这里指的是输入一串字符建立一个链表,可以实现单链表的倒转
//头插法和尾插法,所谓的区别就是建议一个带头节点的链表,采用头插法时每输入一个元素时都在头节点后插入,尾插法则是接着上一次的表尾进行插入
linklist list-headinsert(linklist &L)
{
LNode *S,int x//这里申请了一个新的指针s后面的头插指针都会用到这个
L=(LNode*)malloc(sizeof(LNode));
L->next=null;//建立了一个空链表,接下来就是头插入的操作
scanf("%d",x)
while(x!=999)//999跳过循环
{
s=(LNode*)malloc(sizeof(LNode));//申请空间
s->data=x;
s->next=L->next
L->next=s;
scanf("%d",x)//进行下一数据的输入
}
return true;
}
//尾插法建表,
linklist listtailinsert(linklist &L)
{
int x;
L=(LNode*)malloc(sizeof(LNode));
LNode *s,*r=L;
scanf("%d",&x);
while(x!=999)
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
r->next=s;//插入
r=s;//后移
scanf("%d",&x);
r->next=null;
return L;
}
//本函数的思想,现用intx表示之后存储数据域的部分是整数类型,第二申请一个头节点第三申请一个临时指针和一个尾指针,其次输入第一个x然后调用while函数实现之后数据的输入以及表的建立,while函数里面的内容解读,首先先创建一个指针s指向地址空间,然后赋值给sd的数据部分,然后将r指针后移一位也就是将现有的r指针指向临时常见的s的位置。再输入下一个数据,等所有的数据结束以后将r的next指向null。
//以上就是所有的操作
3.双链表
双链和单链表的区别就在于双链表多了一个数据域指向当前指针的前驱节点,这样做有一个好处,传统的单链表只能从前往后的遍历,访问前驱的复杂度为n,然而添加一个前驱指针以后就变成了1,这里的n和1是指相对于一个p节点他的前驱访问
//双链表的一些操作及其定义
typedef struct DNode{
elemtype data;
struct Dnode *prior,*next
}DNode,Dlinklist
插入图示,代码不唯一,但是思想唯一必须先将s的next指向p的next然后再连接上你p和s因为你不这样做的话现连接p和s你就会断掉剩下的链表。
这里的思想总结为一句话,每一步的操作都必须保证链表不断要保证next的指向在三个节点中永远是清楚的。三个节点分别为p节点p->next节点和新创建的s节点。他们的next和prior域必须清清楚楚。
双链表的删除操作
也是三个节点的关系pq和q的next删除q节点
思想,每一步操作保证链表不断
p->next=q->next;
q-next->prior=p;
free(q);
4.循环链表
所谓的循环链表就是在普通链表的基础上表尾指针指向的不是null而是头节点或者第一个数据节点它的操作(增删改查)和单(双)链表基本上相似所不一样的是表尾的操作,
循环链表的操作分为单循环和双循化
单循环
可以从任意一个节点遍历整个链表,有些同时设立表头指针和表尾指针使得工作效率更高,当只有头指针是在表尾插入需要遍历整个数组,复杂度为n有表尾指针是在表尾插入复杂度为1.
双循环
双链表进行循环,头节点的prior指针指向表尾,表尾的next指针指向表头。链表空时头节点的prior和next都指向头节点自己
5.静态链表
其实就是用数组来描述顺序表的结构所谓静态也就是要提前预分配空间一般用在不支持高级语言的系统中接下来看图