线性表:
一、线性表定义:
1、线性表的定义
定义:线性表(List):零个或多个数据元素的有限序列。
注意:
1)线性表是一个序列。也就是说,线性表的元素之间是有序的。若元素存在多个,对于其中一个元素来说,它前面的元素叫前驱,后面的元素叫后继。第一个元素无前驱,最后一个元素无后继,中间的元素只有一个前驱,一个后继。
2)线性表是有限的。事实上,在计算机科学领域,我们只研究有限的序列,因为计算机的处理对象都是有限的。而对于那些无限的序列,只在数学中讨论。
线性表的元素个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。
练习:说明以下数据集是否是线性表
1、一年中的十二星座
2、一个公司的组织架构(非皮包公司)
3、一个班级间的同学关系
4、班级同学的点名册
5、播放器的播放列表
6、linux的文件系统
答案:
1、4、5是,2、3、6不是
2、线性表的操作:
设有线性表L,对线性表的操作有:
⒈建立一个空线性表
⒉清空一个线性表
⒊判断线性表是否为空
⒋求表长
⒌获得表中第i个位置的元素
⒍确定某元素在线性表中的位置
⒎插入一个新的元素
⒏删除第i个位置的元素
⒐遍历该线性表
二、线性表的顺序存储结构
整体代码先上::
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 20
#define OK 1
#define ERROR 0
typedef int data_t;
typedef struct
{
data_t data[MAXSIZE];//定长情况下
int length;//表长
}SqList;
SqList *CreateEmptySqList()//定长
{
SqList *sq;
if(NULL==(sq = (SqList*)malloc(sizeof(SqList))))
{
printf("CreateEmptySqList Error\n");
exit(0);
}
sq->length=0;
return sq;
}
int GetElem(SqList *L,int i,data_t *e)//获得顺序表第i个位置的元素
{
if(i>=L->length || i<0)
{
perror("No this Element");
return ERROR;
}
*e=L->data[i];
return OK;
}
int ListInsert(SqList *L,int i,data_t e)//在线性表的第i个位置插入元素e
{
int k;
if(L->length==MAXSIZE)//线性表已满
{
printf("SqList is Full\n");
return ERROR;
}
if(i<0 || i>L->length)//i不在线性表的范围内
{
printf("Position Error\n");
return ERROR;
}
if(i<L->length)//若插入位置不在表尾
{
for(k=L->length-1;k>=i;k--)//当前位置开始到表尾所有元素向后移动1格
L->data[k+1]=L->data[k];
}
L->data[i]=e;//插入新元素
L->length++;
return OK;
}
int ListDelete(SqList *L,int i,data_t *e)//删除线性表的第i个位置元素,并用e得到删除的值
{
int k;
if(L->length==0)//线性表为空
{
printf("SqList is Empty\n");
return ERROR;
}
if(i<0 || i>=L->length)//i不在线性表的范围内
{
printf("Position Error\n");
return ERROR;
}
*e=L->data[i];//取走元素
if(i<L->length-1)//若删除位置不在表尾
{
for(k=i;k<L->length-1;k++)
L->data[k]=L->data[k+1];
}
L->length--;
return OK;
}
int PrintList(SqList *L)//遍历打印整个顺序表
{
int k;
data_t temp=0;
if(L->length==0)
{
printf("线性表为空!\n");
return ERROR;
}
for(k=0;k<L->length;k++)
{
GetElem(L,k,&temp);
printf("%d\t",temp);
}
printf("\n");
return OK;
}
int main()
{
int i;
data_t data;
SqList *sq = CreateEmptySqList();
for(i=0;i<5;i++)
{
sq->data[sq->length]=i;
sq->length++;
}
if(GetElem(sq,3,&data)==OK)
{
printf("data is %d\n",data);
}
PrintList(sq);
ListInsert(sq,2,20);
PrintList(sq);
ListDelete(sq,2,&data);
PrintList(sq);
return 0;
}
1、定义存储结构
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
在C语言中,我们可以使用一维数组来实现顺序表的存储结构。即把表的第一个元素数据存放到数组下标为0的位置中,接着把线性表相邻的元素存储到数组中的相邻位置。
线性表的顺序存储结构代码:
#define MAXSIZE 20
typedef int data_t;
typedef struct
{
data_t data[MAXSIZE];
int length;
}SqList;
这里要区分线性表长度与数组长度的区别。数组长度是存放线性表的存储空间长度,在编译后这个存储空间就不变了。而线性表长度指的是线性表中的数据元素个数,随着线性表的操作,这个数据是变化的。在任意时刻,线性表长度应该小于等于数组长度。
对于顺序表来说,逻辑位置上相邻的两个元素,其在存储位置上也是相邻的。例如a[i]和a[i+1],二者在逻辑结构上是前后关系,在存储结构上也是前后关系。
2、插入与删除
有关于顺序表的相关操作,一些操作是十分简单的。例如:求表长,查询第i个位置元素,置空,遍历等。这里不详细讲解。对于顺序表来说,重点要掌握它的插入与删除操作。
1)获得元素
对于线性表的顺序存储结构来说,获取第i个位置的元素是非常简单的。
int GetElem(SqList *L,int i,data_t *e)//获得顺序表第i个位置的元素
{
if(i>=L->length || i<0)
{
perror("No this Element");
return ERROR;
}
*e=L->data[i];
return OK;
}
2)插入操作
举个例子:一队人在排队买票,这时来了一个插队的,他插入到的队伍的第i个位置,那么在他之后的人都要向后挪一步。
在这个例子中我们已经说明了线性表的顺序存储结构在插入数据时的实现过程。
插入算法思路:
⒈如果插入位置不合理,返回异常
⒉如果线性表长度大于等于数组长度,则返回异常或增加容量
⒊从最后一个位置开始向前遍历到第i个位置,分别将它们都向后移动一个位置
⒋将要插入元素填入位置i处
⒌表长加1
int ListInsert(SqList *L,int i,data_t e)//在线性表的第i个位置插入元素e
{
int k;
if(L->length==MAXSIZE)//线性表已满
{
printf("SqList is Full\n");
return ERROR;
}
if(i<0 || i>L->length)//i不在线性表的范围内
{
printf("Position Error\n");
return ERROR;
}
if(i<L->length)//若插入位置不在表尾
{
for(k=L->length-1;k>=i;k--)//当前位置开始到表尾所有元素向后移动1格
L->data[k+1]=L->data[k];
}
L->data[i]=e;//插入新元素
L->length++;
return OK;
}
3)删除操作
接着刚才的例子:此时后边排队的人意见很大,这时他冲出了队伍,消失在人群中。于是排队的人群又想蠕虫一样向前移动了一步,队伍又恢复了平静。
这就是线性表的顺序存储结构删除元素的过程。
删除算法思路:
⒈如果删除位置不合理,返回异常
⒉取出删除元素
⒊从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置
⒋表长减1
int ListDelete(SqList *L,int i,data_t *e)//删除线性表的第i个位置元素,并用e得到删除的值
{
int k;
if(L->length==0)//线性表为空
{
printf("SqList is Empty\n");
return ERROR;
}
if(i<0 || i>=L->length)//i不在线性表的范围内
{
printf("Position Error\n");
return ERROR;
}
*e=L->data[i];//取走元素
if(i<L->length-1)//若删除位置不在表尾
{
for(k=i;k<L->length-1;k++)
L->data[k]=L->data[k+1];
}
L->length--;
return OK;
}
3、线性表顺序存储结构的优缺点
优点:
⒈无需为表示表中元素之间的逻辑关系而额外增加额外的存储空间
⒉可以快速地存取表中任意位置的元素
缺点:
⒈插入和删除操作需要移动大量元素
⒉当线性表长度变化较大时,难以确定存储空间的容量
⒊可能造成存储空间“碎片”
链表:
三、线性表的链式存储结构
代码先上::
//注意:该文件操作的单链表为带头结点单链表,头结点数据无效
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define OK 1
#define ERROR 0
typedef int data_t;
typedef struct Node
{
data_t data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
LinkList CreateEmptyLinklist()//创建一个空表,空表只有头结点
{
LinkList p;
p = (LinkList)malloc(sizeof(Node));
if(p==NULL)
{
perror("CreateEmptyLinkList error");
exit(0);
}
//p->data=-255;//表示无效数据
p->next=NULL;
return p;
}
LinkList CreateListHead(LinkList L,int n)//创建链表(头插法)
{
LinkList p;
int i;
srand(time(NULL));//初始化随机数种子
for(i=0;i<n;i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;
p->next = L->next;
L->next = p;
}
return L;
}
LinkList CreateListTail(LinkList L,int n)//创建链表(尾插法)
{
LinkList p,r;
int i;
srand(time(NULL));
r = L;
for(i=0;i<n;i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p;
}
r->next = NULL;//链表封尾
return L;
}
int ClearList(LinkList L)//清空链表
{
LinkList p,q;
p=L->next;
while(p)
{
q=p->next;
free(p);
p=q;
}
L->next=NULL;
return OK;
}
int GetElem(LinkList L,int i,data_t *data)//读取单链表的第i个元素
{
int j;
LinkList p;//工作指针
p = L->next;
j = 1;
while(p && j<i)
{
p = p->next;//让p指向下一个节点
j++;
}
if(!p)
{
printf("%d position error\n",i);
return ERROR;
}
*data = p->data;
return OK;
}
int ListInsert(LinkList L,int i,data_t e)//插入新节点,使其成为第i个节点
{
int j;
LinkList p,s;
p=L;
j=1;
while(p && j<i)//寻找i的位置
{
p=p->next;
j++;
}
if(!p)//p为NULL
{
printf("%d position error\n",i);
return ERROR;
}
//若if没有执行则证明位置有效,可以插入数据
s=(LinkList)malloc(sizeof(Node));
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
int ListDelete(LinkList L,int i,data_t *e)//删除第i个位置节点,数据由e获得
{
int j;
LinkList p,q;
if(L->next==NULL)
{
printf("LinkList is Empty!\n");
return ERROR;
}
p=L;
j=1;
while(p->next && j<i)
{
p=p->next;
j++;
}
if(!(p->next))
{
printf("%d position error\n",i);
return ERROR;
}
q=p->next;
p->next=q->next;
*e=q->data;
free(q);
return OK;
}
int PrintList(LinkList L)//遍历打印整个链表
{
LinkList p=L->next;
while(p)
{
printf("%d\t",p->data);
p=p->next;
}
printf("\n");
return OK;
}
int ListReverse(LinkList L)//练习1:单链表反序
{
LinkList p,q;
p=L->next;
L->next=NULL;
while(p!=NULL)
{
q=p;
p=p->next;
q->next=L->next;
L->next=q;
}
return OK;
}
LinkList Adjmax(LinkList h)//练习2:寻找最大元素对
{
LinkList p, p1, q;
int m0, m1;
p = h->next;
p1 = p;
if(p1 == NULL)
return p1; //表空返回
q = p->next;
if(q == NULL)
return p1; //表长=1时的返回
m0 = p->data + q->data; //相邻两结点data值之和
while (q->next!=NULL)
{
p = q;
q = q->next; //取下一对相邻结点的指针
m1 = p->data + q->data;
if(m1 > m0)
{
p1 = p;
m0 = m1;
}
}//取和为最大的第一结点指针
return p1;
}
int main()
{
LinkList head1,head2;
int i;
data_t data=12;
head1=CreateEmptyLinklist();
head2=CreateEmptyLinklist();
printf("head1\n");
head1=CreateListHead(head1,15);
PrintList(head1);
printf("head2\n");
head2=CreateListTail(head2,15);
PrintList(head2);
scanf("%d",&i);
printf("Insert head1 %d position, data is %d\n",i,data);
ListInsert(head1,i,data);
PrintList(head1);
scanf("%d",&i);
ListDelete(head1,i,&data);
printf("Delete head1 %d position, data is %d\n",i,data);
PrintList(head1);
LinkList adjmax = Adjmax(head1);
printf("Adjmax data is %d, Adjmax data next data is %d\n",adjmax->data,adjmax->next->data);
ListReverse(head1);
printf("Reserve head1:\n");
PrintList(head1);
if(ClearList(head1)==OK)
{
printf("head1 Clear success\n");
}
if(ClearList(head2)==OK)
{
printf("head2 Clear success\n");
}
return 0;
}
前面所讲的线性表的顺序存储结构是有缺点的,最大的缺点就是插入和删除时需要移动大量的元素,这显然就需要耗费大量时间。
仔细考虑一下产生该问题的原因,在于相邻元素的存储位置也具有邻居关系,它们在内存中是紧挨着的,没有空隙,自然也没有空位进行介入,而删除后留下的空隙自然也需要弥补。
为了解决上述问题,我们打破常规,不再让相邻元素在内存中紧挨着,而是上一个元素留存下一个元素的“线索”,这样我们找到第一个元素时,根据“线索”自然而然就找到下一个元素的位置;依次类推,通过遍历的方法每一个元素的位置都可以通过遍历找到。
1、线性表的链式存储定义
定义:节点(或译为“结点”)(Node):为了表示每个数据元素ai与其后续数据元素ai+1之间的逻辑关系,对数据ai来说,除了存储本身的数据信息之外,还需要存储一个指示其直接后继信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素ai的存储映像,称为节点(Node)。
定义:单链表:n个节点(ai的存储映像)链接成一个链表,即为线性表的链式存储结构。因为此链表的每个节点中只包含一个指针域,所以叫单链表。
定义:头指针:我们把链表中第一个节点的存储位置叫做头指针,整个单链表的存储就必须从头指针开始进行。
但是,单纯使用头指针无法区分一个单链表是否为空 还是 一个单链表不存在。因为二者从头指针的角度来说,都是指针为空,而单链表为空和单链表不存在是两种完全不同的概念。
为了解决这个问题,我们引入头结点head的概念。头结点即单链表的第一个节点,该节点不存储任何有效数据,实际链表的起点是头结点的后继节点。
当头结点的后继节点为空,即
head->next==NULL
时,此时我们判定该链表为空链表。而当头结点head不存在时,此时我们判定该单链表不存在。
2、线性表的链式存储结构代码描述
单链表的存储结构的C语言描述:
typedef struct Node
{
data_t data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
由代码我们可以看出,节点是由存放数据元素的数据域和存放后继节点的指针域组成的。假设指针p是指向第i个元素ai的指针,则p->data表示ai的数据域,p->next表示ai的指针域。p->next指向下一个元素,即ai+1,也就是说,p->data=ai,p->next->data=ai+1。
1)单链表的整表创建
在顺序表中,顺序表的创建其实就是一个数组的初始化过程。而单链表和顺序表的存储结构不同,它不能像顺序表一样整体集中地操作数据,而且单链表是一种动态结构。所以创建单链表的过程实际上是将一个“空表”动态依次建立各元素节点并逐步插入到链表中的过程。
单链表的整表创建的算法思路(头插法):
⒈声明指针p和计数器i
⒉初始化一个空链表头结点head
⒊让head的结点指针指向NULL,即建立一个带头结点的空单链表
⒋循环以下过程:
⒋⒈通过指针p生成新节点
⒋⒉新节点获得数据,即p->data=数据
⒋⒊将p插入到头结点与前一新节点之间
LinkList CreateEmptyLinklist()//创建一个空表,空表只有头结点
{
LinkList p;
p = (LinkList)malloc(sizeof(Node));
if(p==NULL)
{
perror("CreateEmptyLinkList error");
exit(0);
}
//p->data=-255;//表示无效数据
p->next=NULL;
return p;
}
LinkList CreateListHead(LinkList L,int n)//创建链表(头插法)
{
LinkList p;
int i;
srand(time(NULL));//初始化随机数种子
for(i=0;i<n;i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;
p->next = L->next;
L->next = p;
}
return L;
}
以上代码里,我们让新生成的节点始终处在第一个位置,我们把这种方法称为“头插法”。
实际上,按照日常生活中“先来后到”的思想,新生成的节点应当插入到当前链表的尾部。若采用这种方法创建单链表,我们称为“尾插法”。
尾插法的算法思路基本等同于头插法,只需将上文的头插法的算法
⒋⒊将p插入到头结点与前一新节点之间
改为:
⒋⒊将p插入到当前链表的尾节点之后
即可。
LinkList CreateListTail(LinkList L,int n)//创建链表(尾插法)
{
LinkList p,r;
int i;
srand(time(NULL));
r = L;
for(i=0;i<n;i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p;
}
r->next = NULL;//链表封尾
return L;
}
注意代码中L与r的关系,L是指向整个单链表,而r是指向当前链表的尾节点。L不会随着循环变换位置,而r会随着循环实时变换位置。
2)单链表的整表删除
当我们不打算使用一个链表时,我们应当对其进行销毁,也就是在内存中释放这个链表,以便留出空间供其他程序使用。
单链表的整表删除的算法思路:
⒈声明指针p和q;
⒉将链表的第一个节点赋值给p
⒊循环以下过程:
⒊⒈将p的下一个节点赋值给q
⒊⒉释放p
⒊⒊将q赋值给p
int ClearList(LinkList L)//清空链表
{
LinkList p,q;
p=L->next;
while(p)
{
q=p->next;
free(p);
p=q;
}
L->next=NULL;
return OK;
}
注意代码中指针q的作用,q的作用是引导指针p,若无指针q的话,在执行free(p)语句之后,指针p就无法找到其下一个节点p->next的位置了,因为该节点的指针域已经随节点一并释放了。
3)单链表的读取
在线性表的顺序存储结构中,我们要通过任意位置读取元素的值是十分方便容易的,但在单链表中,对于第i个元素具体在哪无法一开始就得知,必须从头指针开始寻找。因此对于单链表的读取第i个元素的操作在算法上相对要麻烦很多。
获得单链表第i个元素的算法:
⒈定义一个指针指向链表的第一个节点。初始化循环变量j
⒉当j<i时,不断让指针p向后移动
⒊若到链表结尾p为空,则说明第i个节点不存在,返回错误
⒋当p移动到i位置成功时,返回节点p的数据
int GetElem(LinkList L,int i,data_t *data)//读取单链表的第i个元素
{
int j;
LinkList p;//工作指针
p = L->next;
j = 1;
while(p && j<i)
{
p = p->next;//让p指向下一个节点
j++;
}
if(!p)
{
printf("%d position error\n",i);
return ERROR;
}
*data = p->data;
return OK;
}
因为该链表的时间复杂度取决于位置i,因此该算法的时间复杂度为O(n)。
因为单链表结构定义时没有定义表长,所以无法事先获知循环次数,因此不推荐使用for循环。该算法的主要核心是“当前工作指针”,这其实也是多数关于链表算法的核心思想。
4)单链表的插入与删除
单链表的插入与删除操作是单链表的优势之一。插入和删除操作无需像线性表的顺序存储结构一样,插入或删除一个节点需要影响到众多其他节点。
假设在单链表中,待插入节点的指针为s,s节点的前驱节点为p,则插入操作只需2步即可:
s->next=p->next; p->next=s;
但是注意,这两句顺序不可交换。
如果先让p->next=s;那么下一句s->next=p->next就相当于s->next=s,这样新加入节点s就无法接入它的后继节点了,“临场掉链子”。
单链表的第i个数据位置插入节点的算法思路:
⒈声明一个指针p指向链表头结点,初始化j从1开始
⒉当j<i时,遍历链表,即指针p不断向后移动
⒊若到链表末尾p为空,则位置i不存在
⒋p找到第i个位置,生成待插入空结点s
⒌将数据元素e赋值给s->data
⒍执行单链表的插入语句:s->next=p->next;p->next=s;
⒎返回成功
int ListInsert(LinkList L,int i,data_t e)//插入新节点,使其成为第i个节点
{
int j;
LinkList p,s;
p=L;
j=1;
while(p && j<i)//寻找i的位置
{
p=p->next;
j++;
}
if(!p)//p为NULL
{
printf("%d position error\n",i);
return ERROR;
}
//若if没有执行则证明位置有效,可以插入数据
s=(LinkList)malloc(sizeof(Node));
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
接下来我们来看单链表的删除。假设第i个位置节点为q,它的前驱节点是p,现在要删除节点q,其实只需将q的前驱节点p绕过q节点指向q的后继节点即可:
q=p->next; p->next=q->next;
单链表的第i个数据位置删除节点的算法思路:
⒈声明一个指针p指向链表头结点,初始化j从1开始
⒉当j<i时,遍历链表,即指针p不断向后移动
⒊若到链表末尾p为空,则说明第i个位置的节点不存在
⒋p指向q的前驱节点,即q==p->next
⒌执行单链表的删除语句:p->next=q->next;
⒍将q节点中数据取出作为结果
⒎释放q节点
⒏返回成功
int ListDelete(LinkList L,int i,data_t *e)//删除第i个位置节点,数据由e获得
{
int j;
LinkList p,q;
if(L->next==NULL)
{
printf("LinkList is Empty!\n");
return ERROR;
}
p=L;
j=1;
while(p->next && j<i)
{
p=p->next;
j++;
}
if(!(p->next))
{
printf("%d position error\n",i);
return ERROR;
}
q=p->next;
p->next=q->next;
*e=q->data;
free(q);
return OK;
}
分析单链表的插入和删除代码,我们可以发现,它们的算法其实都是由两部分组成:第一部分是遍历查找第i个节点,第二部分是对它进行相应的操作。而且我们可以看出,对于第i个节点的操作不会影响到其他位置的节点,这也是单链表比顺序表优势的地方。显然,对于插入/删除比较频繁的操作,单链表的效率要明显高于顺序表。
3、顺序表与单链表的优缺点
通过对比,我们可以得出一些结论:
⒈若该线性表需要频繁进行查找操作,而很少进行插入/删除操作时,我们推荐采用顺序表存储。而若该线性表需要频繁进行插入/删除操作时,我们推荐使用单链表。例如在一款游戏中,玩家个人信息除注册时涉及到插入数据外,一般不会发生大的改变,因此我们使用顺序表存储。而玩家的装备列表则会随着玩家的游戏而发生改变,即随时会发生插入/删除操作,这时使用顺序表就不太合适,而应采用单链表。
⒉当线性表中的元素数量变化较大,或无法事先预制数目时,我们推荐使用单链表,这样可以无需考虑存储空间大小分配的问题。反之,若我们已事先知道了数据规模(例如1年有12月,1星期有7天等情况),则我们推荐使用顺序表,这样存储效率会高很多。
总之,顺序表与单链表各有其优缺点,需要在实际情况中权衡需要使用哪种方式。
练习1:单链表反序
已有单链表L,编写函数使得单链表的元素反序存储。
提示:函数原型:int ListReverse(LinkList L)
int ListReverse(LinkList L)//练习1:单链表反序
{
LinkList p,q;
p=L->next;
L->next=NULL;
while(p!=NULL)
{
q=p;
p=p->next;
q->next=L->next;
L->next=q;
}
return OK;
}
练习2:已有单链表L存放的数据类型为整型,其头结点为head,编写函数,求单链表中相邻两点数据data之和为最大的一对节点的第一节点的指针。
提示:函数原型:LinkList Adjmax(LinkList h)
LinkList Adjmax(LinkList h)//练习2:寻找最大元素对
{
LinkList p, p1, q;
int m0, m1;
p = h->next;
p1 = p;
if(p1 == NULL)
return p1; //表空返回
q = p->next;
if(q == NULL)
return p1; //表长=1时的返回
m0 = p->data + q->data; //相邻两结点data值之和
while (q->next!=NULL)
{
p = q;
q = q->next; //取下一对相邻结点的指针
m1 = p->data + q->data;
if(m1 > m0)
{
p1 = p;
m0 = m1;
}
}//取和为最大的第一结点指针
return p1;
}
4、循环链表
若将单链表的尾节点的指针由空改成指向头结点,则将整个单链表形成了一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(Circular linked list)。
循环链表解决了一个单链表存在的很麻烦的问题:如何从链表的任意节点出发,访问到链表的全部节点。
同单链表一样,为了解决单循环链表的空表与非空表操作一致问题,我们通常会设置一个头结点,该头结点里不存任何数据(或只存其他无关数据)。这样对于循环的判断结束条件就从p->next是否为空,变成了p->next是否为头结点。
//注意:该文件操作的单循环链表为带头结点单循环链表,头结点数据为-1,但在数据操作过程中头结点不参与
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define OK 1
#define ERROR 0
typedef int data_t;
typedef struct CirNode
{
data_t data;
struct CirNode *next;
}CirNode,*CirLinkList;
int GetElem(CirLinkList L,int i,data_t *data)//读取循环链表的第i个元素
{
int j;
CirLinkList p;
p = L;
j = 0;
if(!p->next)
{
printf("链表为空!\n");
return ERROR;
}
else
{
do
{
p=p->next;
j++;
}while(p->next!=L->next && j<i);
}
*data = p->data;
return OK;
}
int ListInsert(CirLinkList L,int i,data_t e)//在第i个节点后面插入节点使新节点成为第i+1号节点
{
int j;
CirLinkList p,s;
if(i==0)//如果要使新节点成为第1个节点,还要操作当前链表表尾的next
{
p=L->next;
while(p->next!=L->next)
p=p->next;
s=(CirLinkList)malloc(sizeof(CirNode));
s->data=e;
s->next=L->next;
L->next=s;
p->next=s;
return OK;
}
else
{
p=L;
j=1;
do
{
p=p->next;
}while(p->next!=L->next && i>j++);
if(j<i)
{
printf("第%d个位置不存在!\n",i);
return ERROR;
}
s=(CirLinkList)malloc(sizeof(CirNode));
s->data=e;
s->next=p->next;
p->next=s;
}
return OK;
}
int ListDelete(CirLinkList L,int i,data_t *e)
{
int j;
CirLinkList p,q;
if(L->next==NULL)
{
printf("链表为空\n");
return ERROR;
}
if(i==1)
{
p=q=L->next;
while(p->next!=L->next)
p=p->next;
L->next=q->next;
p->next=q->next;
*e=q->data;
free(q);
return OK;
}
else
{
p=L;
j=1;
do
{
p=p->next;
j++;
}while(p->next!=L->next && j<i);
if(p->next==L->next)
{
printf("第%d个位置不存在!\n",i);
return ERROR;
}
q=p->next;
p->next=q->next;
*e=q->data;
free(q);
}
return OK;
}
CirLinkList CreateEmptyList()
{
CirLinkList p;
p = (CirLinkList)malloc(sizeof(CirNode));
if(p==NULL)
{
perror("创建循环链表失败");
exit(0);
}
p->data=-1;//无效数据
p->next=NULL;
return p;
}
CirLinkList CreateList(CirLinkList L,int n)
{
CirLinkList p,r;
int i;
srand(time(NULL));
r = L;
for(i=0;i<n;i++)
{
p = (CirLinkList)malloc(sizeof(CirNode));
p->data = rand()%100+1;
r->next = p;
r = p;
}
r->next = L->next;//将表尾接入1号节点
return L;
}
int ClearList(CirLinkList L)
{
CirLinkList p,q;
p=L->next;
while(p->next!=L->next)
{
q=p->next;
free(p);
p=q;
}
free(p);//处理最后一个节点
L->next=NULL;
return OK;
}
int PrintList(CirLinkList L)
{
CirLinkList p=L;
if(!p->next || p->next==p)
{
printf("链表为空\n");
return ERROR;
}
else
{
do
{
p=p->next;
printf("%d\t",p->data);
}while(p->next!=L->next);
}
printf("\n");
return OK;
}
int main()
{
CirLinkList head;
head = CreateEmptyList();
head = CreateList(head,10);
PrintList(head);
int i=5;
data_t data=12;
printf("插入head第%d个位置,数据为%d\n",i,data);
ListInsert(head,i,data);
printf("插入后链表为:\n");
PrintList(head);
i=8;
ListDelete(head,i,&data);
printf("删除head第%d个位置,删除数据为%d\n",i,data);
printf("删除后链表为:\n");
PrintList(head);
if(ClearList(head)==OK)
{
printf("head已被成功删除!\n");
}
return 0;·
}
练习:使用单项循环链表求解约瑟夫环问题,其中人数n为33人,每逢m=7人枪毙一人,起始位置为第k=1个人
/***********约瑟夫环问题描述******************/
约瑟夫入狱,监狱内共有n=33个犯人。某日33名犯人围成一圈,从第k=1个犯人开始报数,报到数字m=7的犯人出列,被枪毙,下一名犯人重新从1开始报数。依次类推,直至剩下最后1名犯人可被赦免。聪明的约瑟夫在心里稍加计算,算出了最后枪毙的位置,他站在这个位置,最终避免了自己被枪毙,逃出升天。
问:约瑟夫算出的是哪个位置?
/***********约瑟夫环问题描述end***************/
#include <stdio.h>
#include <stdlib.h>
typedef int data_t;
/* use a cycle linked list without header node */
typedef struct node_t
{
data_t data;
struct node_t *next;
} linknode_t, *linklist_t;
//注意:该题目使用的为不带头结点的循环链表
int main()
{
int i, n, m, k;
linklist_t p, q;
printf("total N people will join this suicide game, please input N:");
scanf("%d", &n);
printf( "people stand in a circle, assume everyone is assigned\n"
"a sequence number from 1 to %d.\n"\
"which one would you like to start the number off (1~%d):", n, n);
scanf("%d", &k);
printf("every Xth people should kill himself, please decide the X:");
scanf("%d", &m);
if (n < 1 || k > n || m < 1) {
printf("input error!\n");
return -1;
}
printf("game is starting ...\n");
/* added the first one */
//第一部分:创建n个人的循环链表
p = q = (linklist_t)malloc(sizeof(linknode_t));
p->data = 1;
/* added other left people */
for (i = 2; i <= n; i++) {
q->next = (linklist_t)malloc(sizeof(linknode_t));
q = q->next;
q->data = i;
}
/* complete the circle */
q->next = p;
/* find the people ahead of #k */
//第二部分:寻找起始位置
q = p;
while (q->next != p) {
if (q->next->data == k)//寻找起始位置k的前驱节点
break;
q = q->next;
}
//第三部分:开始Johsph环逻辑
while (q->next != q) { /* till the last one */
/* every m people */
for (i = 0; i < m - 1; i++)//删除m号节点需要寻找它的前驱(m-1号)节点
{
q = q->next;
}
/* kill the m-th people */
p = q->next;
q->next = p->next;
printf("%-2d was killed\n", p->data);
free(p);
}
/* kill the last one */
printf("%-2d was alive\n", q->data);
free(q);
return 0;
}
5、双向链表
我们在单链表中,有了next指针,它使得我们要查找某节点的下一个节点的时间复杂度为O(1)。可是若要查找某节点的上一个节点,那时间复杂度就是O(n)了。因为我们每次要查找某节点的上一个节点,必须从头指针开始遍历查找。
为了克服单向性这一缺陷,我们设计出了双向链表。双向链表(double linked list)是在单链表的每个节点中,再设置一个指向其前驱节点的指针域。所以在双向链表的每个节点都有两个指针域,一个指向直接后继,另一个指向直接前驱
/*双向链表的存储结构*/
typedef struct DulNode
{
data_t data;
struct DulNode *prior;
struct DulNode *next;
}DulNode,*DuLinkList;
由于双向链表的节点指针有两个,那么对于某节点p,它的后继的前驱是其本身,它的前驱的后继也是其本身,即:
p->next->prior = p = p->prior->next
因为双向链表是单链表扩展出来的结构,因此它的很多操作与单链表是基本相同的。比如获得位置i的节点的数据、遍历打印整个链表、求表长等操作,这些操作都只涉及到一个方向的指针(prior或next),另一个方向的指针基本无用,因此操作与单链表本身并无区别。
对于双向链表的插入/删除操作,需要更改两个指针变量,因此双向链表的插入/删除操作需要注意两个指针变量的操作顺序。
双向链表的插入操作:向节点p与p->next之间插入节点s
注意4步的顺序不能错:
①s->prior = p;
②s->next = p->next;
③p->next->prior = s;
④p->next = s;
由于第二步与第三步都涉及到p->next,如果先行执行第四步的话,会使得p->next节点提前变成s,使得后续的工作无法完成。所以,实现双向链表的插入操作的顺序是:先解决s的前驱和后继,再解决后节点的前驱,最后解决前节点的后继。
//注意:该文件操作的链表为带头结点双向链表,头结点数据为-1
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define OK 1
#define ERROR 0
typedef int data_t;
typedef struct DulNode
{
data_t data;
struct DulNode *prior;
struct DulNode *next;
}DulNode,*DuLinkList;
int GetElem(DuLinkList L,int i,data_t *data)//读取双向链表的第i个元素
{
int j;
DuLinkList p;
p = L;
j = 0;
while(p && j<i)
{
p = p->next;//让p指向下一个节点
j++;
}
if(!p)
{
printf("第%d个位置不存在!\n",i);
return ERROR;
}
*data = p->data;
return OK;
}
int ListInsert(DuLinkList L,int i,data_t e)
{
int j;
DuLinkList p,s;
p=L;
j=1;
while(p && j<i)
{
p=p->next;
j++;
}
if(!p)
{
printf("第%d个位置不存在!\n",i);
return ERROR;
}
s=(DuLinkList)malloc(sizeof(DulNode));
s->data=e;
s->prior=p;
s->next=p->next;
if(p->next)
p->next->prior=s;
p->next=s;
return OK;
}
int ListDelete(DuLinkList L,int i,data_t *e)
{
int j;
DuLinkList p;
p=L;
j=0;
while(p && j<i)
{
p=p->next;
j++;
}
if(!p)
{
printf("第%d个位置不存在!\n",i);
return ERROR;
}
*e=p->data;
if(p->next!=NULL)
{
p->prior->next=p->next;
p->next->prior=p->prior;
}
else
{
p->prior->next=p->next;
}
free(p);
p=NULL;
return OK;
}
DuLinkList CreateEmptyList()
{
DuLinkList p;
p = (DuLinkList)malloc(sizeof(DulNode));
if(p==NULL)
{
perror("CreateEmptyList error");
exit(0);
}
p->prior=NULL;
p->next=NULL;
//p->data=-1;
return p;
}
DuLinkList CreateList(DuLinkList L,int n)
{
DuLinkList p,r;
int i;
srand(time(NULL));
r = L;
for(i=0;i<n;i++)
{
p = (DuLinkList)malloc(sizeof(DulNode));
p->data = rand()%100+1;
r->next = p;
p->prior = r;
r = p;
}
r->next = NULL;
return L;
}
int ClearList(DuLinkList L)
{
DuLinkList p,q;
p=L->next;
while(p)
{
q=p->next;
free(p);
p=q;
}
L->next=NULL;
return OK;
}
int PrintList(DuLinkList L)
{
DuLinkList p=L->next;
while(p)
{
printf("%d\t",p->data);
p=p->next;
}
printf("\n");
return OK;
}
int main()
{
DuLinkList head;
head = CreateEmptyList();
head = CreateList(head,15);
int i=5;
data_t data=12;
PrintList(head);
printf("插入head第%d个位置,数据为%d\n",i,data);
ListInsert(head,i,data);
printf("插入后链表为:\n");
PrintList(head);
i=8;
ListDelete(head,i,&data);
printf("删除head第%d个位置,删除数据为%d\n",i,data);
printf("删除后链表为:\n");
PrintList(head);
if(ClearList(head)==OK)
{
printf("head已被成功删除!\n");
}
return 0;
}
双向链表的删除操作比较简单,若要删除节点p,只需两个步骤即可:
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
int ListDelete(DuLinkList L,int i,data_t *e)
{
int j;
DuLinkList p;
p=L;
j=0;
while(p && j<i)
{
p=p->next;
j++;
}
if(!p)
{
printf("第%d个位置不存在!\n",i);
return ERROR;
}
*e=p->data;
if(p->next!=NULL)
{
p->prior->next=p->next;
p->next->prior=p->prior;
}
else
{
p->prior->next=p->next;
}
free(p);
p=NULL;
return OK;
}
对于单链表来说,双向链表要复杂一些,对于插入和删除操作要注意其操作顺序。另外每个节点都使用了额外的存储空间来存储前驱节点。但是双向链表有良好的对称性,而且对某节点的前驱节点操作要方便许多。