线性表是零个或多个数据元素的有限序列,首先线性表是一个序列,也就是说,元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他买个元素都有且只有一个前驱和后继。
线性表的顺序存储结构
线性表的顺序存储结构是指用一段地址连续的存储单元依次存储
线性表的数据元素。示意图如下:
我们经常使用的数组就是一类顺序存储的线性表,它是地址连续的一类存储单元,下面将使用程序设计一下这种顺序线性表。
#define MAXSIZE 20 //线性表最大元素个数
typedef int ElemType; //线性表元素类型
typedef struct
{
ElemType data[MAXSIZE]; //线性表
int length; //线性表中已有元素个数
}Sqlist; //线性表管理
这样我们便完成了一个简单线性表的设计。
data 就是我们的线性表,我们主要是对他进行一个增删改查操作。很多算法也是基于顺序线性表进行操作的,例如我们的排序算法,下面来了解下顺序线性表的操作。
获取元素操作
/*
q:线性表指针
x:表示获取线性表第几个元素的值
m:保存获取的值
返回值为-1 失败,为 0 成功
*/
int GetValue(Sqlist *q,int x,ElemType *m)
{
if(q == NULL || q->length == 0 || x >= q->length || x < 0)
return -1;
*m = q->data[x];
return 0;
}
插入操作
/*
q:线性表指针
n:待插入的值
flag:插入方式标值:为 1 表示头插,为 2 表示表示正序插入,为 3 表示倒序插入,
为 4 表示尾插
返回值为-1 失败,为 0 成功
*/
int InsertionValue(Sqlist *q,ElemType n,int flag)
{
if(q == NULL || q->length == MAXSIZE)
return -1;
if(flag == 1) //头插
{
for(int i = q->length;i > 0;i--)
{
q->data[i] = q->data[i-1];
}
q->data[0] = n;
q->length++;
return 0;
}
else if(flag == 2) //正序
{
int i;
for(i = 0;i < q->length;i++) //找到插入位置
{
if(q->data[i] >= n)
break;
}
for(int j = q->length;j > i;j--) //往后移
{
q->data[j] = q->data[j-1];
}
q->data[i] = n;
q->length++;
return 0;
}
else if(flag == 3) //倒序
{
int i;
for(i = 0;i < q->length;i++) //找到插入位置
{
if(q->data[i] <= n)
break;
}
for(int j = q->length;j > i;j--) //往后移
{
q->data[j] = q->data[j-1];
}
q->data[i] = n;
q->length++;
return 0;
}
else //尾插
{
q->data[q->length] = n;
q->length++;
return 0;
}
}
删除指定值操作
/*
q:线性表指针
n:待删除的值
返回值为-1 失败,为 0 成功
*/
int DeleteValue(Sqlist *q,ElemType n)
{
if(q == NULL || q->length == 0)
return -1;
for(int i = 0;i < q->length;i++)
{
if(q->data[i] == n)
{
for(int j = i;j < q->length;j++)
{
q->data[j] = q->data[j+1];
}
q->length--;
i--;
}
}
return 0;
}
清空操作
void empty(Sqlist *q)
{
q->length = 0;
}
线性表顺序存储的优缺点
优点:
1.无须为表示表中元素之间的逻辑关系而增添额外内存空间,向链式存储会需要添加一个链表指针用来连接其他元素。
2.可以快速的存取表中任意位置的元素,因为是地址连续,所以可以使用地址偏移去轻松找到其他元素。
缺点:
1.插入和删除操作需要移动大量元素
2.当线性表长度变化较大时,难以确定存储空间的容量
3.造成存储空间的碎片
线性表的链式存储结构
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这就意味着这些数据元素元素可以存在内存未被占用的任意位置。
为了表示每个数据元素 ai于其直接后继数据元素 ai+1之间的关系,对于 ai而言,其内部不仅存放了自己的数据消息之外,还有一个指针指向其直接后继的地址,所以一个链式线性表每一个节点都由两部分组成,数据域和指针域。
设计一个链式线性表应该如下,对于节点设计要有数据域和指针域,另外,由于链式存储的地址不连续性,是没法在任意位置通过地址偏移去找到其他元素的,只能通过访问节点的指针域找到下一个节点,这样,就需要一个链表管理者结构来记录我们头节点和尾节点的位置,以方便我们对链表的访问,设计如下。
typedef struct Link //链表节点
{
/*数据域*/
char data[10];
int num;
/*指针域*/
struct Link *next
}LinkNode;
typedef struct _Head //节点管理,方便找到头和尾
{
LinkNode *first; //指向头节点
LinkNode *tail; //指向尾节点
int num; //节点个数
}Head
单链表的插入操作示例
/**
* 插入操作
* lk:链表管理
* x:待插入元素
* the:插入方式
*/
void insert_linkedist(Head *lk,int x,int the) //插入元素到链表
{
if(lk == NULL)
{
return;
}
//创建节点
Node *p = (Node*)malloc(sizeof(Node));
p->date=x;
p->next=NULL;
if(the==1) //头插法
{
if(lk->num==0) //如果链表中没有元素
{
lk->first=p;
lk->tail=p;
}
else
{
p->next=lk->first;
lk->first=p;
}
(lk->num)++;
}
else if(the==2) //尾插法
{
if(lk->num==0) //如果链表中没有元素
{
lk->first=p;
lk->tail=p;
}
else
{
lk->tail->next=p;
lk->tail=p;
}
(lk->num)++;
}
else if(the==3) //升序排列
{
if(lk->num==0) //如果链表中没有元素
{
lk->first=p;
lk->tail=p;
}
else
{
Node *q=lk->first; //用于遍历
if(lk->first->date>=p->date) //插入元素比链表所有元素小
{
p->next=lk->first;
lk->first=p;
}
else if(lk->tail->date<=p->date) //插入元素比链表所有元素大
{
lk->tail->next=p;
lk->tail=p;
}
else
{
Node *t=q; //用于遍历,保证 p 插入位置在 t 和 q 之间
while (q)
{
if(q->date>=p->date)
{
q->next=p;
p->next=q;
break;
}
t=q;
q=q->next;
}
}
}
(lk->num)++;
}
else //输入错误
{
return;
}
}
单链表的删除操作示例
/**
* lk:链表管理节点
* x:待删除的值
*/
void delete_all(Head *lk,TYPE x) //删除一个无序链表中指定的值
{
if(lk==NULL || lk->num==0)
return;
Node *qk=lk->first; //指向要删除元素
Node *qr=qk; //指向要删除元素前一个
while (qk)
{
Node *t=qk;
if(qk->date==x)
{
qk=qk->next;
if(qr == t) //说明删除的首节点
{
qr->next == NULL;
qr = qk;
}
else
qr->next=qk;
(lk->num)--;
free(t);
continue;
}
qr=qk;
qk=qk->next;
}
if(lk->num == 0)
{
lk->first = NULL;
lk->tail = NULL;
}
}
单链表的优缺点
从存储分配方式来看:
顺序存储结构用一段连续的存储单元依次存储线性表数据元素。而单链表采用链式结构,用一组任意的存储单元存放线性表的元素
从时间性能来说:
顺序存储的查找时间复杂度为 O(1),而单链表为 O(n),但是顺序存储的插入和删除时间复杂度为 O(n),而链式存储的时间复杂度为 O(1)
从空间性能来看:
顺序存储结构需要分配存储空间,分大了,浪费,分小了易发生上溢。而链式存储不需要一开始分配空间,只要有需要就分配,元素个数也不受限制。
链表的几种形式
上面我们讲述了线性表的链式存储结构以及设计方法和优缺点,对于数组来言,我们可以设计二维数组,指针数组,结构体数组等多种类型的数组,那么链表我们也可以设计成很多不同的样子,在学习数据结构,我们不应该花很多心思去学习每个结构的使用方法,更重要的是要去理解这种结构的一个思想,例如对于链式结构而言,它的思想就是使用指针域来指向下一元素位置,使两个逻辑上连续的空间物理上不一定连续。
静态链表
我们在上面对链表进行学习我们找到,链表中每个节点具有数据域和指针域,每个节点的位置在内存中不连续,所以我们称这种链表为一个动态链表,而静态链表则是在内存上空间连续的一块数组空间,不过使用链表的思想将其演变为一个链表而已,我们称这种链表为静态链表。 其设计如下:
#define NUM_MAX 1000
typedef int TYPE;
typedef struct LinkNode
{
TYPE data; //数据域 保存节点数据
int cur; //游标域 保存下一个元素的数组下标
}sLink;
int NodeNum = 0; //记录节点数量
sLink StaticLink[NUM_MAX];
其中,NUM_MAX 表示静态链表的最大容量,静态链表分为数据域和游标域,游标中存储着此节点的下一个节点的下标,这样在对数据进行操作时,就不需要做到严格意义上的连续,例如,StaticLink[10].cur = 50.这样表示 StaticLink[10]的下一个元素不会是StaticLink[11]了,而是StaticLink[50]。
静态链表的初始化操作:
void init()
{
for(int i=0;i<NUM_MAX-1;i++)
StaticLink[i].cur = i+1;
StaticLink[NUM_MAX-1] = 0;
}
一般静态链表的头元素和尾元素都不会用来存储数据,头元素的 cur 一般用来表示存储空位下标,例如一开始下标为 1 的数组元素要用来保存数据,则 StaticLink[0].cur保存的就是 1,而 StaticLink[NUM_MAX-1].cur 中保存的是存储数据首元素的下标,例如,如果这个静态链表中有很多元素,一开始没有首元素先初始化为 0,StaticLink[10]是作为逻辑上第一个元素,那么 StaticLink[NUM_MAX-1].cur 中保存的就是 10.
静态链表的插入操作:
/**
* x:插入元素
* io:插入方法
* n:插入位置
*/
void Interposition(TYPE x,int io,int n)
{
if(StaticLink[0].cur == NUM_MAX -1) //满
return;
if(io == 1) //尾插
{
if(StaticLink[NUM_MAX-1].cur == 0) //表示插入的是第一个元素
{
StaticLink[StaticLink[0].cur].data = x;
StaticLink[StaticLink[0].cur].cur = 0; //最后一个元素游标保存 0
StaticLink[NUM_MAX-1].cur == StaticLink[0].cur;
StaticLink[0].cur = StaticLink[StaticLink[0].cur].cur;//保存下一个空位
}
else
{
int i = 1; //从第一个元素遍历,找到插入前的最后一个元素
while(1)
{
if(StaticLink[i].cur == 0)
{
StaticLink[i].cur = StaticLink[0].cur;
break;
}
}
StaticLink[StaticLink[0].cur].data = x;
StaticLink[StaticLink[0].cur].cur = 0; //最后一个元素游标保存 0
StaticLink[0].cur = StaticLink[StaticLink[0].cur].cur;//保存下一个空位
}
}
else if(io == 2) //头插
{
if(StaticLink[NUM_MAX-1].cur == 0) //表示第一个元素
{
StaticLink[StaticLink[0].cur].data = x;
StaticLink[StaticLink[0].cur].cur = 0; //最后一个元素游标保存 0
StaticLink[NUM_MAX-1].cur == StaticLink[0].cur;
StaticLink[0].cur = StaticLink[StaticLink[0].cur].cur;//保存下一个空位
}
StaticLink[StaticLink[0].cur].data = x;
StaticLink[0].cur = StaticLink[StaticLink[0].cur].cur; //保存下一个空位
StaticLink[StaticLink[0].cur].cur = StaticLink[NUM_MAX-1].cur; //下一个元素为原来的第一个元素
}
else //插入第 n 个元素之前的位置
{
int f = StaticLink[NUM_MAX-1].cur; //指向插入前的那个元素下标
int q = f; //指向插入后的那个元素下标
if( n<=1 || n>NodeNum)
return;
for(int i = 1; i<n;i++)
{
f = q;
q = StaticLink[f].cur;
}
StaticLink[StaticLink[0].cur].data = x;
StaticLink[StaticLink[0].cur].cur = q;
StaticLink[f].cur = StaticLink[0].cur;
StaticLink[0].cur = StaticLink[StaticLink[0].cur].cur; //保存下一个空位
}
NodeNum ++;
}
静态链表的删除操作:
/**
* x:待删除元素
*
*/
void DeleteNode(TYPE x)
{
if(LinkNode == 0) //无元素
return;
int f = StaticLink[NUM_MAX-1].cur; //指向删除前的那个元素下标
int q = f; //指向删除后的那个元素下标
for(int i = 0;i < LinkNode;i++)
{
if(StaticLink[q].data == x) //x 是要被删除的
{
if(q == StaticLink[NUM_MAX-1].cur) //如果是第一个元素
{
StaticLink[NUM_MAX-1].cur = StaticLink[q].cur;
}
else
{
StaticLink[f].cur = StaticLink[q].cur;
}
int t = StaticLink[0].cur;
StaticLink[0].cur = f; //指向首空位
StaticLink[q].cur = t; //保存下一个空位
continue;
}
f = q;
q = StaticLink[f].cur;
}
}
静态链表优缺点分析:
优点:
在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。
缺点:
没有解决连续存储分配带来的表长难以预测的问题,失去了顺序存储的结构随机存取的特性。
循环链表
循环链表就是将单链表中终端节点的指针由空指针改成指向头节点,就能使整个单链表形成一个环。这种头尾相接的单链表为单循环链表,简称循环链表
双向链表
双向链表是在单链表的每个节点中,再设计一个指向直接前驱的指针域,所以一个双向链表具有两个指针域,一个指向直接前驱,一个指向直接后继。
双向链表定义示例:
typedef int TYPE;
typedef struct node //单链表结构体
{
TYPE date;
struct node *next; //指向直接后继
struct node *prev; //指向直接前驱
}Node;
typedef struct head
{
int num;
struct node *first; //头
struct node *tail; //尾
}Head;
双向链表元素插入示例:
/**
* dlk:双向链表管理
* x:插入元素
* n:插入方式 1:头插 2:尾插 3:升序插入
*/
void insert_double_linkedist(Head *dlk,int x,int n) //插入元素到链表
{
if(dlk==NULL)
return;
Node *p=(Node*)malloc(sizeof(Node));
p->date=x;
p->next=NULL;
p->prev=NULL;
if(n==1)
{
if(dlk->num==0) //插入的为首个元素
{
dlk->first=p;
dlk->tail=p;
}
else
{
dlk->first->prev=p;
p->next=dlk->first;
dlk->first=p;
}
(dlk->num)++;
}
else if(n==2)
{
if(dlk->num==0) //插入的为首个元素
{
dlk->first=p;
dlk->tail=p;
}
else
{
dlk->tail->next=p;
p->prev=dlk->tail;
dlk->tail=p;
}
(dlk->num)++;
}
else if(n==3)
{
if(dlk->num==0) //插入的为首个元素
{
dlk->first=p;
dlk->tail=p;
(dlk->num)++;
return;
}
Node *q=dlk->first; //用于找到插入位置
while (q)
{
if(q->date>=p->date)
{
if(q->prev != NULL)
q->prev->next=p;
p->prev=q->prev;
p->next=q;
q->prev=p;
break;
}
p=p->next;
}
if(q == NULL) //说明待插入元素比链表中所有元素大,插入在链表末尾
{
dlk->tail->next=p;
p->prev=dlk->tail;
dlk->tail=p;
}
(dlk->num)++;
return;
}
else
return;
}
双向链表元素删除示例:
void delete_all(Head *dlk,TYPE x) //删除一个无序链表中指定的值
{
if(dlk==NULL||dlk->num==0)
return;
Node *p=dlk->first;
while (p)
{
Node *t=p;
if(p->date==x)
{
if(p->prev != NULL)
p->prev->next=p->next;
if(p->next != NULL)
p->next->prev=p->prev;
p=p->next;
(dlk->num)--;
free(t);
continue;
}
p=p->next;
}
}