线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。
线性表有两种物理存储结构,顺序存储结构和链式存储结构。
顺序存储结构封装一般需要3个属性:
1.数据起始地址(数组名)
2.最大储存容量(一般初始化后就不变,除非要动态扩容)
3.线性表的当前长度(元素个数是会变化的)
线性表的基本操作:
InitList(*L): 初始化一个线性表
ListEmpty(L): 判断是否为空表,空为True,非空为False
ClearList(*L): 清空线性表
GetElem(L,i,*e): 将表中的第i个元素,返回给e
LocateElem(L,e): 查找表中是否有与e相等的元素。成功,返回序号;若失败,返回0。
ListInsert(L,i,e): 在线性表的第i个位置,插入新元素e
ListDelete(*L,i,*e): 删除线性表中的第i个元素,并将值返回给e
ListLength(L): 返回线性表元素的个数
顺序存储结构:
优点:
1.无需为表示表中元素之间的逻辑关系而增加额外的存储空间
2.可以快速的存取表中的任意位置的元素
缺点:
1.插入和删除需要移动大量元素(中间没有间隙)
2.当线性表长度变化较大的时候,难以确定储存空间的容量
3.容易造成存储空间的“碎片”(两个大型的线性表之间间隔的地址)
链式存储结构属性:数据域和指针域,两者的结合称为Node。两个元素之间的地址未必相连,只能通过前一个元素的指针域指向后一个元素的指针域判断。
头指针为单链表必要元素,指向头结点,头结点的数据域通常描述整个链表的当前元素长度,再指向第一个有效元素结点。
链表尾的结点指针指向NULL(有头结点的空链表,头指针指向头结点;无头结点的空链表,头指针直接指向NULL)
链表结点属性:
typedef struct node
{
Elemtype data; //数据域
struct node *next; //指针域
}node;
typedef struct node* Linklist; (Linklist L == struct node *L);
GetElem():获取单链表第i个元素的数据域
创建一个指针p,p指向L的第一个结点,初始化计数j=1;
当j < i时遍历链表,直到j = i,返回当前数据域;
如果p最后指向NULL或者j>i,则该结点无效。
typedef int Status
typedef int Elemtype;
Status GetElem(Linklist L,int i,Elemtype *e)
{
int j;
Linklist p;//定义临时指针p
p = L->next;
j = 1;
while(p && j < i) //如果p非空 且 j < i
{
p = p->next;
j++;
}
if(!p || j > i)
{
return ERROR; //#define ERROR 1;
}
*e = p->data;
return OK; //#define OK 0;
}
时间复杂度:O(n)
ListInsert():将元素插入单链表某个位置
Status ListInsert(Linklist *L,int i,Elemtype e)
{
int j;
Linklist temp,new;
j = 1;
temp = *L;
while(temp && j < i) //找到j=i,temp是目标结点的前一个结点
{
temp = temp->next;
j++;
}
if(!temp || j > i)
{
return ERROR;
}
new = (Linklist)malloc(sizeof(node));//动态申请内存,链表的意义(随时扩容)
new->data = e;
new->next = temp->next;
temp->next = new;
return OK;
}
时间复杂度O(n)
ListDelete():单链表删除某个结点
Status ListDelete(Listlink *L,int i,Elemtype *e)
{
int j;
Linklist temp,temp2;
j = 1;
temp = *L;
while(temp && j < i) //找到要删除的前一结点
{
temp = temp ->next;
j++;
}
if(!temp || j > i)
{
return ERROR;
}
temp2 = temp->next; //要删除的结点
*e = temp2->data;
temp->next = temp2 ->next;
free(temp2);
return OK;
}
时间复杂度O(n)
创建单链表的过程是一个动态生成链表的过程,从“空表”的初始状态,依次建立各元素结点并逐个插入链表。
算法思路:
1.声明结点p和计数器i;
2.初始化空链表L;
3.将L的头结点指针域指向NULL,形成一个带头结点的空链表;
4.循环实现后继结点的赋值和插入;
头插法,将新元素放在表头的第一个位置
CreatListHead():
void CreatListHead(Linklist *L,int n)
{
Linklist p;
int i;
srand(time(0));
*L = (Linklist)malloc(sizeof(struct node)); //申请头结点
(*L)->next = NULL; //头结点指向NULL
for(i = 0;i < n;i++)
{
p = (Linklist)malloc(sizeof(struct node));
p->data = rand()%100+1;
p->next = (*L)->next;
(*L)->next = p;
}
}
尾插法,将元素放在最后一个位置
Void CreatListTail(Linklist *L,int n)
{
Linklist p,temp;
int i;
*L =(Linklist)malloc(sizeof(struct node));
(*L)->next = NULL;
temp = *L;//指向当前链表的末尾结点
srand(time(null));
for(i = 0; i < n ;i++)
{
p = (Linklist)malloc(sizeof(struct node));//新结点
p->data = rand%100 + 1;
temp->next = p;
temp = p;
}
temp->next = NULL;//最后结点指向NULL
}
单链表的整表删除(内存释放)
Status ClearList(Linklist *L)
{
Linklist p,q;
p = (*L)->next;//指向头结点
while(p)
{
q = p->next;
free(p);
p = q;
}
return OK;
}
链式存储结构与顺序存储结构优缺点对比:
1.时间性能
(1)查找
顺序存储结构O(1)
单链表O(n)
(2)插入和删除
顺序存储结构O(n),要进行数据位置的平移
单链表O(1),找到位置,直接操作
2.空间性能
(1)顺序存储结构要预先规划
(2)单链表无限制,可动态扩展
所以,项目中如果经常需要查找操作,可使用顺序存储结构;如果要进行动态的操作,则适合链式存储结构。
静态链表
1.静态链表规定数组的第一个元素和最后一个元素的data不能有数据。
2.元素的游标指向数组下标。
3.数组尾的游标指向第一个有数据元素的下标。
4.数组头的游标指向静态链表的结尾(有数据的后一个,即备用链表的第一个结点的下标)。
5.最后一个有数据的元素游标指回下标0(链表头)。
6.其余有效元素游标都是指向游标的下一个有效元素的下标(第一个元素的游标是50,下一个元素的下标即是50)。
通常把未使用数组元素称为备用链表。
静态链表存储结构:
#define Maxsize 1000
typedef int Elemtype;
typedef struct
{
Elemtype data; //数据
int cur; //游标
}component,staticLinklist[Maxsize];//下标
静态链表初始化:
Status staticLinklistInit(staticLinklist space)
{
int i;
for(i = 0;i < Maxsize-1;i++)//下标从0开始
{
space[i].cur = i+1;//游标从1开始
}
space[Maxsize-1].cur = 0;//目前是空的
return OK;
}
静态链表插入,首先先要找到备用链表的首下标
int malloc_SLL(staticLinklist space)//获取备用链表的下标
{
int i;
i = space[0].cur;//将第一个备用链表的下标赋给i
if(space[0].cur)
{
space[0].cur = space[i].cur;//将静态链表头指向下一个备用链表
}
return i;
}
Status staticLinklistInsert(staticLinklist L,int i,Elemtype e)
{
int j,k,l; //第i个元素之前插入新元素
k = Maxsize -1;
if(i < 1 || i > Maxsize) //插入位置不合法
{
return ERROR;
}
j = malloc_SLL(L); //获取要填充的下标
if(j)
{
L[j].data = e; // e是新插入的元素
for( l=1; l <= i-1; l++ ) // 要插到第一个结点后,i=2,则循环执行1次
{
k = L[k].cur; // 找到插入位置的前一个结点下标
}
L[j].cur = L[k].cur; // 插入的新结点的游标指向原来的第i个
L[k].cur = j; //将i-1的结点游标指向要填充的下标
return OK;
}
return ERROR;
静态链表的删除(数据依然存在,但已不在有效元素链上)
Status staticLinklistDelete(staticLinklist L,int i,Elemtype *e)
{
int j,k;
if(i < 1 || i > Maxsize)
{
return Error;
}
k = Maxsize - 1;
for(j = 1; j < i - 1;j++)//先找要删除的前一个有效结点的下标,
//例删除上表的第2个结点,i=2
{
k = L[k].cur; //k=1
}
j = L[K].cur; //找到要删除结点的下标,j=2
//L[j].Data = 0;
L[k].cur = L[j].cur;//完成删除
L[j].cur = L[0].cur;//连接下一备用结点
L[0].cur = j; //将删除的结点作为第一个备用链表;
}
计算静态链表的有效元素个数
int staticListLen(staticLinklist L)
{
int i = 0;
int j = L[Maxsize-1].cur;
while(j)
{
j = L[j].cur;
i++;
}
return i;
}
静态链表优点:
1.在插入和删除操作时,不需要移动元素,只需要改变游标,从而改进了在顺序存储结构中要移动大量元素的缺点。
静态链表缺点:
1.没有解决动态扩展,数组长度不能动态扩展
2.不能直接通过数组下标进行随机存储。
快慢指针找未知长度单链表的中间结点:
设置两个指针同时指向链表的头结点,其中快指针的移动速度是慢指针的两倍,当快指针指到末尾结点时,慢指针正好在中间结点。
Status Getmidnode(ListLink L,Elemtype *e)
{
ListLink search,mid;
search = mid = L;
while(search->next != NULL)
{
mid = mid->next;
if(search->next->next != NULL)
{
search = search->next->next;
}
ELSE
{
search = search->next;//快指针的后后结点为空,后结点不为空时,找尾
}
}
*e = mid->data;
return OK;
}
循环链表:
将链表的尾结点的指针域指回头结点,使其形成一个环,这种称为单循环链表,也成为循环链表。
typedef int Elemtype;
typedef struct node
{
Ememtype data;
struct node *next;
}Node;
typedef struct node* LinkClist;
CycleListInit();
void CycleListInit(LinkCList *CL)
{
LinkCList temp , rear;
Elemtype i;
printf("输入结点的值,输入0则完成初始化\n");
while(1)
{
scanf("%d",&i);
//fflush(stdin);
scanf("%*[^\n]%*c");
srand(time(0));
if(i == 0)
{
return;
}
if((*CL) == NULL)//空表
{
*CL = (LinkCList)malloc(sizeof(Node));
if(!(*CL))
{
printf("内存创建失败");
exit(1);
}
(*CL)->data = i;
(*CL)->next = *CL;
}
else
{
for(rear = (*CL); rear->next != (*CL);rear = rear->next);//找到末尾结点
temp = (LinkCList)malloc(sizeof(Node));
if(!temp)
{
printf("内存创建失败");
exit(1);
}
temp->data = i;
temp->next = *CL;//新结点指向头
rear->next = temp;//原来末尾的指向新结点
}
}
}
CycleListInsert();
void CycleListInsert(LinkCList *CL,int i)
{
LinkCList temp,rear,newnode;
int j,newdata;
printf("输入插入结点的值:");
scanf("%d",&newdata);
if(i == 1)
{
newnode = (LinkCList)malloc(sizeof(Node));
if(!newnode)
{
printf("插入结点内存分配失败\n");
exit(1);
}
newnode->data = newdata;
for(rear = *CL;rear->next != *CL;rear = rear->next);
newnode->next = *CL;
rear->next = newnode;
*CL = newnode;
}
else
{
temp = *CL;
for(j = 1; j < i-1;j++)
{
temp = temp->next;
}
newnode = (LinkCList)malloc(sizeof(Node));
if(!newnode)
{
printf("插入结点内存分配失败\n");
exit(1);
}
newnode->data = newdata;
newnode->next = temp->next;
temp->next = newnode;
}
}
CycleListDelete();
void CycleListDelete(LinkCList *CL,int i)
{
LinkCList temp,target;
int j = 1;
if(i == 1)
{
for(target = *CL;target->next != *CL;target = target->next);
temp = *CL;
*CL = (*CL)->next;
target->next = *CL;
free(temp);
}
else
{
target = *CL;
for(j = 1; j < i-1;j++)
{
target = target->next;
}
temp = target->next;
target->next = temp->next;
free(temp);
}
}
双向链表
同时拥有前驱和后驱,第一个有效节点的前驱指向末尾,末尾结点的后驱指向第一个有效指针。
结点结构:
typedef struct Dualnode
{
Elemtype data;
struct node *prior;//前驱
struct node *next;//后驱
}DualNode;
typedef struct Dualnode * LinkDList;
插入操作:
s->next = p;
s->prior = p->prior;
p->prior->next = s;
p->prior = s;
删除操作:
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);