天天看点

学习笔记---链表

链表

链表基础

定义:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

特点:

结点地址不连续

结点:

定义:组成链表的基本单元

组成:用户需要用的实际数据和下一个结点的地址

图示:

学习笔记---链表

解析:

1.每个正方形代表一个结点、其上方的数字代表其在内存中的地址值。正方形的两层分别代表其数据内容以及用于指向下一个结点的指针。

2.如图,每个结点都存储着下个结点的地址,以此形成逻辑上的链状。

3.第一个正方形head代表头指针变量,用于存放链表中第一个结点的地址。

实现方式:链表用含指针的结构体实现。

例:

struct Node//声明结构体,用于模拟结点
{
    char data;//用于存储数据,可以声明多个
    struct Node *next;//用于存储下一个结点的地址
};
           

代码示例:

#include <stdio.h>
#include <stdlib.h>
/*这个程序用来测试简单单向链表的创建及使用*/
struct Student//使用结构体来模拟结点的结构
{
    int num;
    float score;
    struct Student *next;//用于存储下一结点的地址
};
int main()
{
    struct Student a,b,c,*head,*p;//a,b,c作为结点,head作为头指针变量,p用于链接各结点
    /*为各结点赋值*/
    a.num=31001,a.score=89.5;
    b.num=31003,b.score=90;
    c.num=31007,c.score=85;

    /*将结点链接为链表*/
    head=&a;//这一步十分重要!链表必须要有头指针变量
    a.next=&b,b.next=&c;//中间的每个结点都存储着下一个结点的地址
    c.next=NULL;//c作为最后一个结点,为其next指针赋值为NULL以表示链表已到尽头

    /*使用链表(输出链表中的值)*/
    p=head;//使p指向链表的头一个结点
    do
    {
        printf("%d %.1f\n",p->num,p->score);
        p=p->next;//核心代码,使p指向链表的下一个结点。这样下一次循环将直接读取到新结点的数据。
    }
    while(p!=NULL);//当p==NULL即链表已到尽头。则不需要继续循环
    return 0;
}
           

结果:

学习笔记---链表

解析:链表的长度可变,结点可自行设计、增加、删除。使用十分灵活。

注:大部分演示以单向链表为例,方便理解。

链表操作

基础

核心:

1.通过指针访问链表

2.利用动态分配空间建立动态链表

代码示例:

#include <stdio.h>
#include <stdlib.h>
/*这个程序用于演示结点控制基础*/

struct Student//结构体Student用于模拟链表结点
{
    int num;
    float score;
    struct Student *next;
};
int main()
{
    struct Student *p;//定义指针p用于操作链表结点
    p=malloc(sizeof(struct Student));//利用malloc为结点动态分配空间
    p->num=31001;//为链表结点添加数据
    p->score=89.5;
    printf("%d %.1f\n",p->num,p->score);//输出测试
    free(p);//对于失去利用价值的结点,直接释放其内存空间
    return 0;
}
           

结果:

学习笔记---链表

解析:

1.对于链表的所有节点,定义指针并分配空间而非直接定义变量。避免因结点过多造成的代码繁杂和空间浪费

2.使用malloc动态分配空间和使用free释放无用结点是链表操作的关键,能保证对链表成员的高控制度。

创建及遍历链表:

代码示例:

#include <stdio.h>
#include <stdlib.h>
/*这个程序用来测试对链表的创建和遍历操作*/

typedef struct NODE//因为结构中需要定义结构体指针,所以即使使用了自定义类型,结构体的名称仍不能省略
{
    int data;
    struct NODE *next;//此处指针时指向结构体NODE的,切勿写成自定义类型Node
}Node;
Node *createLinkList(int);//用于创建链表
void traverse(Node*);//用于遍历并输出链表中的值的函数
int main()
{
    Node *head;
    int n=0;
    printf("输入链表的结点数:");
    scanf("%d",&n);
    head=createLinkList(n);
    printf("开始输出链表:\n");
    traverse(head);
    return 0;
}
Node *createLinkList(int n)//n代表链表的结点数
{
    Node *h=NULL,*p,*last;//h表示链表头结点,last表示链表末结点
    int d,i;
    for(i=0;i<n;i++)
    {
        scanf("%d",&d);
        p=(Node *)malloc(sizeof(Node));//创建一个结点
        p->data=d;//为结点赋值
        p->next=NULL;//初始化next
        if(h==NULL)//如果链表无头结点
            h=p;//使本次创建的结点成为链表头结点
        else
            last->next=p;//否则,使本次创建的结点和之前的末结点链接
        last=p;//使本次创建的结点成为末结点
    }
    return h;//返回头结点的地址
}
void traverse(Node *h)
{
    Node *p;
    p=h;//p首先指向头结点
    while(p!=NULL)
    {
        printf("%-5d",p->data);
        p=p->next;//p指向下一个结点
    }
    printf("\n");
    return;
}
           

结果:

学习笔记---链表

解析:

1.createLinkList函数用于创建链表,实质是一个循环创建并链接结点的过程。其中h作为链表头指针变量其值一旦确定就不再改变,而last始终指向链表末结点。

2.traverse函数用于遍历链表,原理与基础部分的测试代码相同。需要注意的是,h作为链表头指针变量。是不应该改变其值的,否则容易造成链表结构混乱或结点丢失。

向链表中插入结点:

原理演示:

p指向r,要将q插入p和r之间

q->next=p->next;

p->next=q;

执行完毕,则链表变为p指向q指向r

代码示例:

#include <stdio.h>
#include <stdlib.h>
/*这个程序用于测试向链表中插入结点*/
/*通过逐个插入结点建立有序链表*/
typedef struct NODE
{
    int data;
    struct NODE *next;
} Node;
Node *insertNode(Node*,int);//用于插入结点
void traverse(Node*);//用于遍历并输出链表中的值的函数
int main()
{
    int i,b[]={67,25,78,99,87};
    Node *head=NULL;
    for(i=0;i<5;i++)
        head=insertNode(head,b[i]);
    traverse(head);
    return 0;
}
void traverse(Node *h)
{
    Node *p;
    p=h;//p首先指向头结点
    while(p!=NULL)
    {
        printf("%-5d",p->data);
        p=p->next;//p指向下一个结点
    }
    printf("\n");
    return;
}
Node *insertNode(Node *h,int b)
{
    Node *q1=h,*q2,*p;
    p=(Node*)malloc(sizeof(Node));//生成新结点
    p->data=b;
    if(h==NULL)//如果当前链表为空,则p作为首结点
    {
        h=p;
        p->next=NULL;
    }
    else if(p->data<h->data)//如果当前结点数值小于首结点数值,则p作为首结点
    {
        h=p;
        p->next=q1;
    }
    else//否则,逐个比对,找到p结点应在位置,将其插入
    {
        /*循环改变q1指向的结点,直到找到p的位置或到达链表末尾*/
        while(q1!=NULL&&p->data>=q1->data)
        {
            q2=q1;
            q1=q1->next;
        }
        /*此时q2正好应当是p的前一结点,因此执行插入*/
        p->next=q2->next;
        q2->next=p;
    }
    return h;
}
           

结果:

学习笔记---链表

解析:

1.通过逐个插入结点,建立有序单向链表。

2.通过操作q1而非直接操作h,保证链表头结点不混乱。

3.对于单向链表的插入,保存上一个结点的地址是很重要的(单向链表和数组不同,无法通过当前结点反推前一结点),因此上述代码中的q2必不可少。

在链表中删除结点:

原理演示:

p指向q指向r,要将q删除

p->next=q->next;

free(q);

执行完毕,则链表变为p指向r,且q不复存在

代码示例:

#include <stdio.h>
#include <stdlib.h>
/*这个程序用于测试向链表中插入结点*/
/*通过逐个插入结点建立有序链表*/
typedef struct NODE
{
    int data;
    struct NODE *next;
} Node;
Node *insertNode(Node*,int);//用于向链表中有序的插入结点
Node *deleteNode(Node*,int);//用于从有序链表中删除指定结点
void traverse(Node*);//用于遍历并输出链表中的值的函数
int main()
{
    int i,b[]={67,25,78,99,87};
    Node *head=NULL;
    for(i=0;i<5;i++)
        head=insertNode(head,b[i]);
    traverse(head);
    int num;
    do
    {
        printf("please enter a num,enter 0000 to exit:");
        scanf("%d",&num);
        if(num==0000)
            return 0;
        head=deleteNode(head,num);
        traverse(head);
    }while(1);
    return 0;
}
void traverse(Node *h)
{
    Node *p;
    p=h;//p首先指向头结点
    while(p!=NULL)
    {
        printf("%-5d",p->data);
        p=p->next;//p指向下一个结点
    }
    printf("\n");
    return;
}
Node *insertNode(Node *h,int b)
{
    Node *q1=h,*q2,*p;
    p=(Node*)malloc(sizeof(Node));//生成新结点
    p->data=b;
    if(h==NULL)//如果当前链表为空,则p作为首结点
    {
        h=p;
        p->next=NULL;
    }
    else if(p->data<h->data)//如果当前结点数值小于首结点数值,则p作为首结点
    {
        h=p;
        p->next=q1;
    }
    else//否则,逐个比对,找到p结点应在位置,将其插入
    {
        /*循环改变q1指向的结点,直到找到p的位置或到达链表末尾*/
        while(q1!=NULL&&p->data>=q1->data)
        {
            q2=q1;
            q1=q1->next;
        }
        /*此时q2正好应当是p的前一结点,因此执行插入*/
        p->next=q2->next;
        q2->next=p;
    }
    return h;
}
Node *deleteNode(Node *h,int b)
{
    Node *p,*q;
    p=h;
    if(h==NULL)//如果链表是空的
        printf("List is null,delete fail.\n");
    else
    {
        /*循环比对结点值,直到比对成功或链表遍历*/
        while(b!=p->data&&p->next!=NULL)
        {
            q=p;
            p=p->next;
        }
        if(b==p->data)//如果比对成功
        {
            if(p==h)//如果该结点是头结点
                h=p->next;//使下一个结点成为头结点
            else
                q->next=p->next;//否则,删除p结点
            free(p);//释放无用结点的内存空间,以避免空间的浪费。
        }
        else//如果比对失败,即链表遍历
            printf("%d not found,delete fail.\n",b);
    }
    return h;
}
           

结果:

学习笔记---链表

解析:删除节点所需要循序的原则和插入结点类似,确保头结点不随意改变以及保存当前结点的上一结点的地址。

链表应用

代码示例:

/*

猴子选大王

m个猴子围成一圈,
编号为1,2,3,.....,m
从第一个开始数数(1。。2。。。3。。4。。),
每数到第n个,该猴子就要离开此圈,
然后下一个猴子重新从1开始数.
这样依次下来,直到圈中只剩最后一只猴子,
则该猴子成为猴子大王。
*/
/*此问题实际上是“约瑟夫问题”的一种,*/

/*
此处用链表(循环单链表)来解决这个问题。
但应认识到这并不是此种问题的最优解
*/
#include <stdio.h>
#include <stdlib.h>

struct Monkey//定义结点
{
    int num;
    struct Monkey *next;
};
struct Monkey* create_circle(int,int);//创建循环单链表(猴子圈)
int vote(struct Monkey*,int,int);//选举猴王
int main()
{
    int m,n,king;
    struct Monkey *head;
    printf("请输入猴子的总数m和猴子选举时数的最大数n,两数中间用空格隔开:\t");
    scanf("%d %d",&m,&n);
    if(n==1)//如果只有一只猴子,直接使他成为猴王。
        king=m;
    else
    {
        head=create_circle(m,n);
        king=vote(head,m,n);
    }
    printf("猴王是%d\n",king);
    return 0;
}
struct Monkey* create_circle(int m,int n)
{
    int i;
    struct Monkey *h,*p1,*p2;
    p1=p2=(struct Monkey*)malloc(sizeof(struct Monkey));
    h=p1;//第一个创建的结点作为首结点
    h->num=1;
    for(i=1;i<m;i++)
    {
        p1=(struct Monkey*)malloc(sizeof(struct Monkey));
        p1->num=i+1;
        p2->next=p1;//p2此时代表的是上一次创建的结点,而p1代表这一次创建的结点。
        p2=p1;//使p2等于p1,这样下次循环时,p2又将刚好是p1的上一个结点。
    }
    p2->next=h;//此时,p2代表末结点。使得p2的下一个结点是首结点,就完成了一个环状的链表
    return h;
}
int vote(struct Monkey* h,int m,int n)
{
    int i,j,k;
    struct Monkey *p1,*p2;
    p1=h;
    for(i=1;i<m;i++)//因为要把m猴子淘汰到只剩一只,所以每次循环淘汰一只猴子,需要m-1次循环
    {
        for(j=1;j<n-1;j++)//模拟从1数到n-1
            p1=p1->next;//循环结束时,p1正好是数n-1的那只猴子。
        p2=p1->next;//使p2指向p1的下一个结点,这样p2就成为了应该出圈的猴子
        p1->next=p2->next;//使p2出圈
        free(p2);//释放p2的内存空间,避免浪费
    }
    k=p1->num;//循环结束,最后剩下的p1就是猴王
    free(p1);//既然选出了猴王,那么p1也就没用了,所以释放其内存空间
    return k;
}
           

结果:

学习笔记---链表

解析:

1.用循环单链表来表示一群猴子。模拟结点的结构体有两个成员,一个保存猴子的编号,一个作为指向下一个猴子的指针。

2.此处使用的是循环单链表,即末结点的指针并不为NULL,而是指向首结点。以此构成环形的链。

3.仍要强调:建立链表和操作链表的核心是:利用好上一个操作的结点的地址值。

链表拓展

树型链表的存储:

图示:

学习笔记---链表

解析:

以上即树形链表,对于如1、2、3、5这样其下链接着其他结点的结点称为根结点。而另一些没有链接更多结点的结点称为叶结点。

原理:

建立结点数组,使得数组的每个元素都代表链表中的一个结点。

如果此结点为根节点,则使该结点的指针指向其下的一个结点(如果有多个结点,则一一指向)。

如果此结点为叶结点,则不连接到其他结点。

图示:

学习笔记---链表

结点结构体:

struct Node
{
    int index;
    Node *next;
};
Node *header[11];
.....
           

二叉排序树

图例:

学习笔记---链表

解析:

对于数列45、24、53、12、28、90。。首先将45作为首结点,

然后将24和45对比,24小于45,因此置于45左下。

然后将53和45对比,53大于45,因此置于45右下。

然后将12和45对比,12小于45,因45左下已有值,

所以将12和24对比,12小于24,因此置于24左下。

然后将28和45对比,28小于45,因45左下已有值,

所以将28和24对比,28大于24,因此置于24右下。

然后将90和45对比,90大于45,因45右下已有值,

所以将90和53对比,90大于53,因此置于53右下。

特性:

1.二叉树中每个结点左下的所有的数,必定小于它。而右下所有的数,必定大于它。

2.当在此二叉树中查找某数时,只要与一个数比对,便可缩减一半的数据规模(类比二分法)

结点结构体:

struct node
{
    struct node *lchild;
    type data;//type可为任何类型的数据,此处只是基本结构示例。
    struct node *rchild;
};
           

搜索函数示例:

int search(node *p,type k)//type可为任何类型的数据,此处只是基本结构示例。
{
    if(p==NULL)
        return -1;
    else if(k==p->data)
        return k;//查找成功
    else if(k<p->data)
        return search(p->lchild,k);//使用递归实现向左下查找
    else if(k<p->data)
        return search(p->rchild,k)
}
           

倒排表(搜索引擎常用数据结构)

图示:

学习笔记---链表

解析:

对于三个句子:

text1: I am you.     text2: He is she.    text3: She is he.

其中出现过的所有单词为:i,am,you,he,is,she

将之存放于数组中,并以指针指向其对应的结点。

每个结点代表该词某次出现的信息。

如am的第一个结点,text1;2;^;代表的就是am这次出现在text1中,在字符串的第二个位置,以及下一次出现的位置为空。

而he的第一个结点,text2;0;指向;代表的就是he这次出现在text2中,在字符串的第零个位置,以及指向下一次出现的位置的指针。

注:对于以上图例中的数组,可以看出其中的单词排列是有序的。因此在查找时可用二分查找等算法进一步加快检索速率。

结点结构体:

struct appear
{
    string filename;
    int start;
    struct appear *next;
};
           

图例:

学习笔记---链表

解析:

对于现实中的搜索引擎,常如上图所示,将单词使用哈希算法换算成一个哈希值,并以哈希值作为数组下标。

这样可以更进一步的提高检索速率。

图例:

学习笔记---链表

解析:

对于现实中的搜索引擎,常如上图所示,将单词使用哈希算法换算成一个哈希值,并以哈希值作为数组下标。

这样可以更进一步的提高检索速率。

继续阅读