线性表的逻辑结构
线性表是n个元素的有限序列。
逻辑关系:表中有且仅有一个开始结点,有且仅有一个终端结点;除开始结点外,每个结点都有一个前驱结点;除终端 结点外,表中的每个结点都有一个后继结点。
顺序表
- 定义:
顺序存储是所有的结点元素存放在一块连续的存储区域中,用存储结点的物理位置来体现结点之间的逻辑关系的存储方法。在高级语言中,一块连续的存储空间通常可用一个数组来表示。因此,顺序存储通常用一个数据元素类型的数组来存储 - 顺序表与C语言中数组的区别:
顺序表是指对逻辑结构表现为线性特点的元素存储的时候要求顺序,其特点是数据在内存存放时体现出来的关系特性。而数组是在分配空间的时候系统自动按照顺序给划出来的一片连续空间。他的作用是存放数据的。关键是在于数组对应的是空间,不对应数据元素。
顺序表是数据结构中的专有名词,而数组是在C语言或者其它编程语言中的一种数据类型。可以说,数组是顺序表在实际编程中的具体实现方式。
线性表顺序存储定义图
- 顺序存储结构是存储结构类型中的一种,该结构是把逻辑上相邻的结点存储在物理位置上相邻的存储单元中,结点之间的逻辑关系由存储单元的邻接关系来体现。
这是一块连续的存储地址
i(元素在线性表中的位置) | a1 | a2 | … | an |
---|---|---|---|---|
data | 1 | 2 | … | n |
C语言定义线性表的顺序存储结构
#define MAXSIZE 10
typedef struct
{
int elem[MAXSIZE];//这里的数据类型可以为别的
int length;//线性表的长度
}SeqList;
顺序表的基本操作
- 顺序表的初始化
顺序表的初始化就是把顺序表 初始化为空的顺序表;只需把顺序表的长度length置为0即可;
void Init_SeqList(SeqList *L) //初始化顺序表 ,SeqList *L定义指针
{
L->length = 0;
}
- 顺序表的插入(在特定位置插入数据)
首先考虑是否满足插入的条件,再进行插入操作
在顺序表的第i个位置插入元素e,首先将顺序表第i个及以后位置的元素依次向后移动一个位置,然后将元素e插入第i个位置,移动元素要从后往前移动元素, 在插入元素之后要将表长L->length++;
int InsertElem(SeqList *L,int i,int x)
{
int j;
if(L->length == MAXSIZE - 1)
{
printf("表满");
return 0;
}
if(i < 1 || i > L->length - 1)
{
printf("输入的位置不合法");
return 0;
}
for(j = L->length;j >= i; j --)
{
L->elem[j+1] = L->elem[j];
}
L->elem[i] = x;
L->length ++;
return 1;
}
- 顺序表的删除
首先判断线性表是否为空,再进行删除操作
顺序表的删除和插入有点类似,删除特定位置的元素时思路是覆盖掉所要删除的元素,即将>i的元素先前移动一位,且使线性表长度-1;
void DeleElem(SeqList *L,int i)
{
if(i > L->length)
{
printf("不存在您要找的元素");
exit(-1);
}
for(int j = i; j < L->length; j ++)
{
L->elem[j] = L->elem[j + 1];
}
L->length --;
}
上述代码时无返回值的删除,如果想要返回删除元素的值,可以加返回值
- 查找
查找时可以根据内容查找,也可以根据位置查找,思路都是一样的
下面的代码为元素的查找
int FindEelm(SeqList L,int x)//寻找元素
{
for(int i = 0; i < L.length; i ++)
{
if(L.elem[i] == x)
{
return i;
}
}
return -1;//未找到元素返回-1
}
- 顺序表的头插与尾插
首先判断是否可以插入元素
头插:每次插入元素将第二个元素及以后的位置都往后移动一位
尾插:每次将待插入的元素放到末尾
void PushFront(SeqList *L, int e)//头插法
{
if (L->length == MAXSIZE)
{
printf("顺序表已满,不能插入!\n");
}
//将表中元素依次后移一位
for (int k = L->length; k > 0; k--)
{
L->data[k] = L->data[k - 1];
}
//插入元素
L->data[0] = e;
L->length++;
}
oid PushBack(SeqList *L, int e)//尾插法
{
if (L->length == ListSize)
{
printf("顺序表已满,不能插入!\n");
}
L->data[L->length] = e;
L->length++;
}
删除时可以进行头删和尾删,思路和头插和尾插一样,进行元素的移动
- 打印顺序表中的元素
void Print(SeqList L)
{
if(L.length == 0)
{
printf("该顺序表为空");
}
else
{
for(int i = 0; i < L.length; i ++)
{
printf("%d ",L.elem[i]);
}
}
}
主函数的调用
int main(void)
{
SeqList L;
Init_SeqList(&L) ;//chu初始化链表
for(int i = 1; i <= 5; i ++)
{
append(&L,i+3);//添加数值时也可以从键盘录入
}
print(L);
printf("%d\n",find(L,8));
deleElem(&L,4);
print(L);
PushFront(&L,89);
print(L);
}
顺序表存储的优缺点:
优点:存储密度大,存储空间利用率高。
缺点:删除和插入元素时不方便
线性表的链式存储
- 链式存储结构的定义
使用指针表示元素之间的逻辑关系,各个数据元素的存储位置可以随意,逻辑上相邻的元素不要求在物理位置上也相邻
单链表
单链表由一系列在内存中相连的结点组成, 每个结点均含有数据域和指针域, 数据域存储数据, 指针域存储指向下一个结点的指针。
- 结点定义如下
typedef struct node
{
int data;//数据域
struct node *next;//指针域
}LNode,*LinkList;
加入头结点可以方便操作,头结点可以存储标题,表长等信息,也可以不存储任何信息。
- 指针变量的操作
单链表的基本操作
- 单链表的头插与尾插法,头插法每次让元素都插入到头结点的后面。
尾插法每次将结点插入到链表的尾部。
LinkList Head_LinkList()//头插法
{
LinkList head = (LinkList)malloc(sizeof(LNode));//生成头结点
head->next = NULL;
int i;
LinkList s;
scanf("%d",&i);
while(i != -1)
{
s = (LinkList)malloc(sizeof(LNode));
s->data=i;
s->next = head->next;
head->next = s;
scanf("%d",&i);
}
return head;
}
LinkList Tail_LinkList1()//尾插法
{
LinkList head = (LinkList)malloc(sizeof(LNode));//生成头结点
head->next = NULL;
int i;
LinkList s,r;
r=head;
scanf("%d",&i);
while(i != -1)
{
s = (LinkList)malloc(sizeof(LNode));
s->data=i;
r->next=s;
r=s;
scanf("%d",&i);
}
r->next=NULL;
return head;
}
void print(LinkList head)
{
LinkList list = head ->next;
while(list != NULL)
{
printf("%d ",list->data);
list = list->next;
}
}
int main(void)
{
LinkList head;
//head = Creat_LinkList();
head = Creat_LinkList1();
print(head);
}
- 单链表的删除
先找到待删除结点的前一个结点,判断是否存在,若存在则进行结点的删除操作,最后一定要释放删除结点的空间
int Dele_LinkList(LinkList H,int i)
{
LinkList p,q;
p = Get_LinkList(H,i-1);
if(p == NULL || p->next == NULL)
{
printf("不存在删除结点");
return 0;
}
else
{
q = p->next;
p->next = q->next;
free(q);
return 1;
}
}
- 插入指定的元素到指定的位置
思路和删除很相似,先找到插入的位置,再进行结点的插入
int Insert_LinkList(LinkList H,int i,int x)//插入元素
{
LinkList p,s;
p = Get_LinkList(H,i - 1);//查找第i-1个 结点
if(p == NULL)
{
printf("插入位置错误");
return 0;
}
else
{
s = (LinkList) malloc(sizeof(LNode));
s->data = x;
s->next = p->next;//插入到第i个结点之后
p->next = s;
return 1;
}
}
循环链表
- 单向循环链表
单向循环链表让单向链表的尾结点指向头结点,为了使空链表与非空链表处理一致,通常设一个头结点,但并不一定要头结点
现在循环链表的终止条件未p->next != phead;
尾插法:每次将插入的元素的next域指向头结点
#include <stdio.h>
#include <stdlib.h>
typedef struct node
{
int data;
struct node *next;
}LNode,*LinkList;
LinkList Head_LinkList()//头插法
{
LinkList head = (LinkList)malloc(sizeof(LNode));//生成头结点
head->next = head;
int i;
LinkList s;
scanf("%d",&i);
while(i != -1)
{
s = (LinkList)malloc(sizeof(LNode));
s->data=i;
s->next = head->next;
head->next = s;
scanf("%d",&i);
}
return head;
}
LinkList Tail_LinkList()//尾插法
{
LinkList head = (LinkList)malloc(sizeof(LNode));//生成头结点
head->next = head;
int i;
LinkList s,temp = head;
scanf("%d",&i);
while(i != -1)
{
s = (LinkList)malloc(sizeof(LNode));
s->data=i;
temp->next=s;
s->next=head;
temp=s;
scanf("%d",&i);
}
return head;
}
void print(LinkList head)
{
printf("链表输出为:\n");
LinkList list = head;
while(list->next != head)
{
list = list->next;
printf("%d ",list->data);
}
}
int main(void)
{
LinkList phead;
// phead = Head_LinkList();
phead = Tail_LinkList();
print(phead);
}
单向循环链表的其他操作和单向链表差不多
双向链表
- 双向链表实在单链表的每个结点中再设置一个指向其前驱结点的指针域。
typedef struct node
{
int data;
struct node *prior;//前驱指针
struct node *next;
}LNode,*LinkList;
- 双向链表的插入操作
s->prior = p;//把p值赋给s的前驱,如图1
s->next=p->next;//将p->next赋值给s的后继,如图3
p->next->prior=s;//将s赋值给p->next的前驱,如图4
p->next = s;//把s赋值给p的后继,如图2
- 双向链表的删除
p->prior->next=p->next;
p->next->prior=p->prior;
- 链表的优缺点
链表
优点:
1.物理存储单元上非连续,而且采用动态内存分配,能够有效的分配和利用内存资源;
2.节点删除和插入简单,不需要内存空间的重组。
缺点:
1.不能进行索引访问,只能从头结点开始顺序查找;
2.数据结构较为复杂,需要大量的指针操作,容易出错。
- 链表与顺序表的使用范围
顺序表:
适用于需要大量访问元素的 而少量增添/删除元素的程序。
链表:
适用于需要进行大量增添/删除元素操作 而对访问元素无要求的程序
静态链表
typedef struct
{
int data;
int cur;
}component;
我们对数组第一个和最后一个元素进行处理,不存数据。我们把未被使用的数组元素成为备用链表
上面的图片为初始化的数组状态
//创建备用链表
void InitList(component *spce)
{
for (int i=0; i<maxSize; i++)
{
array[i].cur = i + 1;//将每个数组分量链接到一起
}
array[maxSize - 1].cur = 0;//链表最后一个结点的游标值为0
}
/提取分配空间
int Malloc_SLL(component * space)
{
//若备用链表非空,则返回分配的结点下标,否则返回0(当分配最后一个结点时,该结点的游标值为0)
int i = space[0].cur;
if (space[0].cur)
{
space[0].cur = sapce[i].cur;
}
return i;
}
//初始化静态链表的数据
int initArr(component *spce)
{
InitList(spce);
int body = Malloc_SLL(spce);
//声明一个变量,把它当指针使,指向链表的最后的一个结点,因为链表为空,所以和头结点重合
int tempBody = body;
for (int i=1; i<5; i++)
{
int j = Malloc_SLL(spce);//从备用链表中拿出空闲的分量
spce[tempBody].cur = j;//链接在链表的最后一个结点后面
spce[j].data = i;//给新申请的分量的数据域初始化
tempBody = j;//将指向链表最后一个结点的指针后移
}
spce[tempBody].cur = 0;//新的链表最后一个结点的指针设置为0
return body;
}
//插入数据
void Insert(component *space, int body, int add, int a)
{
int tempBody = body;
for (int i=1; i<add; i++)
{
tempBody = array[tempBody].cur;//找到第add个元素之前的位置
}
int insert = Malloc_SLL(space);
space[insert].cur =space[tempBody].cur;
space[insert].data = a;
space[tempBody].cur = insert;
}
优点:在进行插入和删除时,只需要修改游标,不需要移动元素。