链表及代码

一、线性表的链式存储结构

前面所讲的线性表的顺序存储结构是有缺点的,最大的缺点就是插入和删除时需要移动大量的元素,这显然就需要耗费大量时间。

仔细考虑一下产生该问题的原因,在于相邻元素的存储位置也具有邻居关系,它们在内存中是紧挨着的,没有空隙,自然也没有空位进行介入,而删除后留下的空隙自然也需要弥补。

为了解决上述问题,我们打破常规,不再让相邻元素在内存中紧挨着,而是上一个元素留存下一个元素的“线索”,这样我们找到第一个元素时,根据“线索”自然而然就找到下一个元素的位置;依次类推,通过遍历的方法每一个元素的位置都可以通过遍历找到。

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插入到头结点与前一新节点之间

//代码见附录

以上代码里,我们让新生成的节点始终处在第一个位置,我们把这种方法称为“头插法”。

实际上,按照日常生活中“先来后到”的思想,新生成的节点应当插入到当前链表的尾部。若采用这种方法创建单链表,我们称为“尾插法”。

尾插法的算法思路基本等同于头插法,只需将上文的头插法的算法

⒋⒊将p插入到头结点与前一新节点之间

改为:

⒋⒊将p插入到当前链表的尾节点之后

即可。

//代码见附录

注意代码中L与r的关系,L是指向整个单链表,而r是指向当前链表的尾节点。L不会随着循环变换位置,而r会随着循环实时变换位置。

2)单链表的整表删除

当我们不打算使用一个链表时,我们应当对其进行销毁,也就是在内存中释放这个链表,以便留出空间供其他程序使用。

单链表的整表删除的算法思路:

⒈声明指针p和q;

⒉将链表的第一个节点赋值给p

⒊循环以下过程:

⒊⒈将p的下一个节点赋值给q

⒊⒉释放p

⒊⒊将q赋值给p

//代码见附录

注意代码中指针q的作用,q的作用是引导指针p,若无指针q的话,在执行free(p)语句之后,指针p就无法找到其下一个节点p->next的位置了,因为该节点的指针域已经随节点一并释放了。

 

3)单链表的读取

在线性表的顺序存储结构中,我们要通过任意位置读取元素的值是十分方便容易的,但在单链表中,对于第i个元素具体在哪无法一开始就得知,必须从头指针开始寻找。因此对于单链表的读取第i个元素的操作在算法上相对要麻烦很多。

获得单链表第i个元素的算法:

⒈定义一个指针指向链表的第一个节点。初始化循环变量j

⒉当j<i时,不断让指针p向后移动

⒊若到链表结尾p为空,则说明第i个节点不存在,返回错误

⒋当p移动到i位置成功时,返回节点p的数据

//代码见附录

因为该链表的时间复杂度取决于位置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;

⒎返回成功

//代码见附录

 

接下来我们来看单链表的删除。假设第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节点

⒏返回成功

//代码见附录

分析单链表的插入和删除代码,我们可以发现,它们的算法其实都是由两部分组成:第一部分是遍历查找第i个节点,第二部分是对它进行相应的操作。而且我们可以看出,对于第i个节点的操作不会影响到其他位置的节点,这也是单链表比顺序表优势的地方。显然,对于插入/删除比较频繁的操作,单链表的效率要明显高于顺序表。

 

3、顺序表与单链表的优缺点

//见附图2

通过对比,我们可以得出一些结论:

⒈若该线性表需要频繁进行查找操作,而很少进行插入/删除操作时,我们推荐采用顺序表存储。而若该线性表需要频繁进行插入/删除操作时,我们推荐使用单链表。例如在一款游戏中,玩家个人信息除注册时涉及到插入数据外,一般不会发生大的改变,因此我们使用顺序表存储。而玩家的装备列表则会随着玩家的游戏而发生改变,即随时会发生插入/删除操作,这时使用顺序表就不太合适,而应采用单链表。

⒉当线性表中的元素数量变化较大,或无法事先预制数目时,我们推荐使用单链表,这样可以无需考虑存储空间大小分配的问题。反之,若我们已事先知道了数据规模(例如1年有12月,1星期有7天等情况),则我们推荐使用顺序表,这样存储效率会高很多。

总之,顺序表与单链表各有其优缺点,需要在实际情况中权衡需要使用哪种方式。

练习1:单链表反序

已有单链表L,编写函数使得单链表的元素反序存储。

提示:函数原型:int ListReverse(LinkList L)

//代码见附录

练习2:已有单链表L存放的数据类型为整型,其头结点为head,编写函数,求单链表中相邻两点数据data之和为最大的一对节点的第一节点的指针。

提示:函数原型:LinkList Adjmax(LinkList h)

//代码见附录

 

二、循环链表

若将单链表的尾节点的指针由空改成指向头结点,则将整个单链表形成了一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(Circular linked list)。

循环链表解决了一个单链表存在的很麻烦的问题:如何从链表的任意节点出发,访问到链表的全部节点。

同单链表一样,为了解决单循环链表的空表与非空表操作一致问题,我们通常会设置一个头结点,该头结点里不存任何数据(或只存其他无关数据)。这样对于循环的判断结束条件就从p->next是否为空,变成了p->next是否为头结点。

//代码见附录

练习:使用单项循环链表求解约瑟夫环问题,其中人数n为33人,每逢m=7人枪毙一人,起始位置为第k=1个人

/***********约瑟夫环问题描述******************/

约瑟夫入狱,监狱内共有n=33个犯人。某日33名犯人围成一圈,从第k=1个犯人开始报数,报到数字m=7的犯人出列,被枪毙,下一名犯人重新从1开始报数。依次类推,直至剩下最后1名犯人可被赦免。聪明的约瑟夫在心里稍加计算,算出了最后枪毙的位置,他站在这个位置,最终避免了自己被枪毙,逃出升天。

问:约瑟夫算出的是哪个位置?

/***********约瑟夫环问题描述end***************/

//代码见附录

 

三、双向链表

我们在单链表中,有了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

//见附图3

注意4步的顺序不能错:

①s->prior = p;

②s->next = p->next;

③p->next->prior = s;

④p->next = s;

由于第二步与第三步都涉及到p->next,如果先行执行第四步的话,会使得p->next节点提前变成s,使得后续的工作无法完成。所以,实现双向链表的插入操作的顺序是:先解决s的前驱和后继,再解决后节点的前驱,最后解决前节点的后继。

//代码见附录

双向链表的删除操作比较简单,若要删除节点p,只需两个步骤即可:

//见附图3

p->prior->next = p->next;

p->next->prior = p->prior;

free(p);

//代码见附录

对于单链表来说,双向链表要复杂一些,对于插入和删除操作要注意其操作顺序。另外每个节点都使用了额外的存储空间来存储前驱节点。但是双向链表有良好的对称性,而且对某节点的前驱节点操作要方便许多。

 

//注意:该文件操作的单链表为带头结点单链表,头结点数据无效

代码

#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;



int GetElem(LinkList L,int i,data_t *data)//读取单链表的第i个元素

{

int j;

LinkList p;

p = L;

j = 1;

while(p && j<i)

{

p = p->next;//让p指向下一个节点

j++;

}

if(!p)

{

printf("%d position is 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;//包括头结点,若不想包括头结点则让p=L->next即可

j=1;

while(p && j<i)//寻找i的位置

{

p=p->next;

j++;

}

if(!p)//说明p为NULL,即没有第i个节点,位置无效

{

printf("%d position is 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;

p=L;

j=1;

while(p->next && j<i)

{

p=p->next;

j++;

}

if(!(p->next))

{

printf("%d position is error\n",i);

return ERROR;

}

q=p->next;

p->next=q->next;

*e=q->data;

free(q);

return OK;

}



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 PrintList(LinkList L)//遍历打印整个链表

{

    LinkList p=L;

    while(p)

    {

        printf("%d\t",p->data);

        p=p->next;

    }

    printf("\n");

    return OK;

}



int ListReverse(LinkList L)//练习1:单链表反序

{

if(!L)

{

printf("LinkList is not exist\n");

return ERROR;

}

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=5;

    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);

    printf("Insert head1 %d position, data is %d\n",i,data);

    ListInsert(head1,i,data);

    PrintList(head1);

    i=8;

    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;

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值