数据结构之链表

10 篇文章 0 订阅
10 篇文章 0 订阅

链表

0 链表

  • 组成
  • 结点:数据域 + 指针域(指向后一个结点的位置)
  • 头指针:用来指向链表的起始位置。
  • 头结点:在链表中的第一个结点前插入一个结点,其数据域是不存数据或者用来存储链表长度,指针域指向第一个结点。头结点可有可无。
  • 首元结点:没有实际的意义,表示存储数据的第一个结点。
  • 其他结点:注意:最后一个结点的指针域是指向NULL,表示链表结束。

1 单链表

1.定义

* 链表的每一个结点只包含一个指针域。

2.基本操作

  • (1)插入元素
    • 步骤1:先遍历找到要插入的位置,使Temp指针一步一步移动到要插入位置,同时需要判断此时的Temp是否为空,为空则表示插入的位置无效(说明插入位置大于链表长度)。
    • 步骤2:将新结点的next指针指向插入位置后的结点。
    • 步骤3:将插入位置前结点的next指针指向插入结点。
    • 步骤4:链表长度加1,返回链表的头指针。
  • (2)删除元素
    • 步骤1:同插入元素的步骤1。
    • 步骤2:将待删除的结点从链表中摘除,即找到该结点的直接先驱Temp的指针域指向该结点的后驱。Temp->next = Temp->next->next;。
    • 步骤3:手动释放被删除的结点内存,防止内存泄漏。
    • 步骤4:链表长度减1,返回链表的头指针。
  • (3)查找元素
    • 从表头遍历链表中的结点,每个结点的数据域与被查数据对比,直到遍历相等(即成功找到,返回此时的位置序号)或者遍历至链表的末端NULL,则未找到。
    • 由于链表可能不知道长度,所以用while循环更好。
  • (4)更新元素
    • 步骤1:遍历链表,找到待更新元素的位置。可以采用for循环。
    • 步骤2:直接更改该结点的数据域。
  • (5)单链表的反转(翻转、逆置链表)
    • 方法1:迭代反转法
    • 步骤1.创建3个结点类型的指针,分别初始化。
    • *不带头结点 *beg = NULL; *mid = head; end = head->next;
    • *带头结点:*beg = head->next;*mid = beg->next;end = NULL;(end用在循环中未反转的链表)
    • 步骤2.开始遍历,先将中间结点指向前一个结点实现反转(mid->next = beg;),然后判断遍历终止条件(end = NULL);并将上面3个指针向后移动一个结点(采用迭代:beg = mid; mid = end; end = end->next);
    • 步骤3:遍历退出后:修改头指针指向,head = mid;返回头指针。
    • 方法2:就地逆置法
    • 思想:依次将链表的结点摘除移动到头部。不增加新链表,但需要两个指针实现。
    • 步骤1. 两个指针初始化为:beg = head; end = head->next;
    • 步骤2. 循环调换:beg->next = end->next;(相当于摘除了end结点)end->next = head; head =end; end = beg->next;
    • 步骤3. return head;
    • 方法3:头插法
    • 思想:依次将链表的头部摘下,然后采用从头部插入的方式新生成一个链表。
    • *步骤1. 创建1个新链表,1个临时结点。link *pNewHead = NULL; link pTemp = NULL;
    • 步骤2. 遍历循环:将原链表的第一个结点摘下存在pTemp中,再插入到新链表中。pTemp = head; head = head->next; (摘得一个结点)pTemp->next = pNewHead; pNewHead = pTemp;(存在新链表中)
    • 步骤3. return pNewHead;

2 静态链表

  • 创建

    • 静态链表的结点组成是由数据域整形游标组成。
    • 备用链表:是指将静态链表中未存放的数据连接起来的链表,其头指针指向a[0]。
    • 数据链表:是指静态链表中存放数据的结点连接起来的链表,其头指针指向a[1]。
    • 静态链表的特点:a[0]的结点位置上是不存放数据的。
  • 创建步骤

    • 步骤1.在静态链表未初始化前,数组中的位置都是空闲态,因此全连接在备用链表上。
    • 步骤2.逐渐添加数据,将备用链表中的a[0]后继结点摘除,作为数据链表的当前最后一个结点。即备用链表的a[0]的后继结点向后移动一个。
  • 代码实现

#include<stdio.h>
#define MAXSIZE 6
//定义一个结点
typedef struct Link {
    int data;
    int cur;
}link;

//1.创建备用链表
void RsvLink(link* arr)
{
    for (int i = 0; i < MAXSIZE; i++)
    {
        arr[i].data = 0xFF; //初始化为脏值
        arr[i].cur = i + 1;
    }
    arr[MAXSIZE - 1].cur = 0; //链表最后一个指向游标0
}
//2.从备用链表中摘除被分配的结点
int MallocArr(link* arr)
{
    int i = arr[0].cur; //记录备用链表中第二结点对应的游标,当分配到链表最后一个结点时,该值为0.
    if (arr[0].cur) //如果不是最后一个结点
    {
        arr[0].cur = arr[i].cur; //备用链表删除一个结点后,首结点中存的游标向下一个结点移动。
    }
    return i;
}
//3.初始化静态链表
int InitArr(link* arr)
{
    RsvLink(arr);
    int Head = MallocArr(arr); //空静态链表的头结点中存在的游标
    int Tail = Head;//空链表,所以头尾一样
    for (int i = 1; i < 4; i++)
    {
        int j = MallocArr(arr); //从备用链表拿出结点的游标
        arr[Tail].cur = j;
        arr[j].data = i; //数据域初始化
        Tail = j; //指向链表后一个结点的游标后移
    }
    arr[Tail].cur = 0; //新的链表最后结点游标为0
    return Head;
}
//4.打印链表
void DisPlayArr(link* arr, int body)
{
    int Temp = body;
    while (arr[Temp].cur)
    {
        printf("%d,%d\t", arr[Temp].data, arr[Temp].cur);
        Temp = arr[Temp].cur;
    }
    printf("%d,%d\t", arr[Temp].data, arr[Temp].cur); //打印最后一个结点,游标是0的结点
}
//创建静态链表
void main()
{
    link arr[MAXSIZE];
    int Head = InitArr(arr);
    printf("静态链表是:\n");
    DisPlayArr(arr, Head);
}
  • 基本操作
    • 添加
    • 删除
    • 查找
    • 更改

静态链表和动态链表的区别

静态链表动态链表
内存预先申请足够空间需要新增结点时开空间
链表数量2条(备用链表,数据链表)1条

3 双向链表

1.背景

  • 单链表的指针域只有一个指向后继元素,因此适合从前往后找数据。因此想从后往前找数据就需要双链表。

2.定义

  • 双向链表是存在两个指针域的链表,各个结点之间的关系是双向的,但头指针一般设置1个,除非实际情况需要。

3.组成

  • 指针域(prior):指向当前结点的前驱结点;
  • 数据域(data):当前结点的数据元素;
  • 指针域(next):指向当前结点的后驱结点;

4.创建思想

  • 与单链表相似,只是多了如下步骤:
  • 将当前的新结点的prior指针指向前驱结点;
  • 同时将当前新结点的直接前驱结点next指针指向当前新结点。

5.代码实现

#include<stdio.h>
#include<stdlib.h>

//1.创建结点
typedef struct Link
{
    struct Link *prior;
    int data;
    struct Link *next;
}link;

//2.创建双向链表
link* InitDoubLink(link *pHead, int len)
{
    pHead =(link*)malloc(sizeof(link));
    if(!pHead)
    {
        return 0;
    }
    //对首元结点初始化
    pHead->prior = NULL;
    pHead->next = NULL;
    pHead->data = 1;
    
    if (len < 1)
    {
        return pHead;
    }

    link *pTemp = pHead; //声明一个指针指向链表最后一个结点
    for (int i = 2; i <= len; i++)
    {
        //创建其他结点并赋值
        link *pBody = (link*)malloc(sizeof(link));
        if (!pBody)
        {
            return 0;
        }
        pBody->prior = NULL;
        pBody->next = NULL;
        pBody->data = i;
        pTemp->next = pBody; //前驱结点指向当前结点
        pBody->prior = pTemp; //当前结点指向前驱结点
        pTemp = pTemp->next; //最后一个结点向后移动
    }

    return pHead;
}
//3.打印双向链表
void Diaplay(link* pHead)
{
    link *pTemp = pHead;
    while (pTemp)
    {
        if (!pTemp->next)
        {
            printf("%d\n", pTemp->data);
        }
        else 
        {
            printf("%d <-> ", pTemp->data);
        }
        pTemp = pTemp->next;
    }
}
int main()
{
    int len = 5;//链表长度
    link* pLink = NULL;//创建一个头指针
    pLink = InitDoubLink(pLink,len); //调用链表创建函数
    Diaplay(pLink);//打印链表
    printf("链表中第4个结点直接的前驱是:%d", pLink->next->next->next->prior->data);
    return 0;
}

6.基本操作

1.添加结点
  • (1)添加至表头
  • 步骤1.将新头结点temp的next指向表头结点head,head的prior指针指向temp。
  • 步骤2.将head移至temp,重新指向新的表头。
  • (2) 添加在表中间
  • 步骤1.新结点与其前驱结点建立双层逻辑;
  • 步骤2.新结点与其后继结点建立双层逻辑。
  • (3)添加在表的末尾
  • 步骤1.找到双链表中最后一个结点;
  • 步骤2.让新结点与最后一个结点进行双向逻辑关系。
 link* insertlink(link *head, int data, int add)
 {
    //新建temp结点
    link *temp = (link*)malloc(sizeof(link));
    temp->data = data;
    temp->prior = NULL;
    temp->next = NULL;

    //插入到链表头,要特殊考虑
    if(add == 1) 
    {
        temp->next = head; //新结点的后继指针指向原先的头指针
        head->prior = temp; //原先头指针的前驱指针指向新结点
        head = temp; //将原头结点移至新的temp结点(首结点)
    }
    else
    {
        link *body = head; //创建一个临时指针指向头结点
        for (int i = 1; i < add - 1; i++) //找到要插入位置的前一个结点
        {
            body = body->next;
        }
        //判断条件为真,说明插入位置为链表尾
        if (body->next == NULL) 
        {
            body->next = temp;
            temp->prior = body;
        }
        else //即插入的位置不是链表的尾部,而是中间位置
        {
            body->next->prior = temp; 
            temp->next = body->next;
            body->next = temp;
            temp->prior = body;
        }
    }
    return head;
 }
2.删除结点
  • 实现思想:遍历找到该结点然后将结点删除,并将该结点的内存释放。
link *DelLink(link *pHead, int data)
{
    link *pTemp = pHead;
    //遍历链表,找到该值
    while(pTemp)
    {
        if(pTemp->data == data)
        {
            pTemp->prior->next = pTemp->next;
            pTemp->next->prior = pTemp->prior;
            free(pTemp);
            return pHead;
        }
        pTemp = pTemp->next;
    }
    printf("链表中没有该元素");
    return pHead;
}
3.查找结点
  • 实现思想:因为双链表和单链表一样,只有一个头指针,所以查找需要从头结点开始遍历查找。
    *代码实现
int FindElemLink(link *pHead,int data, int location)
{
    link *pTemp = pHead;
    location = 1; //第一个结点位置为索引1
    while(pTemp)
    {
        if(pTemp->data == data)
        {
            return location;
        }
        location++;
        pTemp = pTemp->next;
    }
    return -1;
}
4.修改链表的结点元素
  • 实现思想:先查找到该结点的位置,直接将数据域改写
  • 代码实现
link *PlaceElemLink(link *pHead, int Loc,int data)
{
    link *pTemp = pHead;
    for(int i = 1; i < Loc; i++) //提前知道位置,所以用for循环
    {
        //将pTemp指针移至到要被修改的数据的位置
        pTemp = pTemp->next;
    }
    temp->data = data;//修改结点上的数据
    return pHead;
}

4 循环链表

1.定义

  • 链表中最后一个结点的指针域指向头结点,整个链表形成一个环。

2.特点

  • 从链表中的任一结点出发均可以找到其他结点。
  • 循环链表成环状,但本质上还是链表,因此在循环链表中,依然能够找到头指针和首元节点等。

3.实例

  • 题目:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人。
  • 解题分析
  • 这是一个循环的过程,当出列其中一个人后,还得继续报数,依次出列。相当于一个循环链表,找到特定位置后,删除该结点,然后继续查找。
  • 代码实现
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>

//1.创建结点
typedef struct Node
{
    int number;
    struct Node* next;
}Person;
//2.创建循环链表并初始化
Person* InitNodeLink(int len)
{
    //创建首元结点
    Person *pHead = (Person*)malloc(sizeof(Person));
    if (!pHead)
    {
        return 0;
    }
    pHead->number = 1;
    pHead->next = NULL;

    Person* pTail = pHead;//创建一个指针指向头指针
    for (int i = 2; i < len; i++)
    {
        //创建一个新结点
        Person* pTemp = (Person*)malloc(sizeof(Person));
        if (!pTemp)
        {
            return pHead;
        }
        pTemp->number = i;
        pTemp->next = NULL;
        //将新结点作为最后一个结点
        pTail->next = pTemp;
        pTail = pTail->next;//此时未结点为刚才的新结点pTemp
    }
    //首尾相连
    pTail->next = pHead;
    return pHead;
}
//3.查找链表中的元素并删除
void FindDelElemLink(Person* head, int k, int m)
{
    Person *tail = head;
    if (!tail)
    {
        return;
    }
    //找到链表第一个结点的上一个结点,即尾结点,为删除操作做准备
    while (tail->next != head)
    {
        tail = tail->next;
    }
    //找到编号为k的人,记为p指针;编号为k的前1个人的位置记为tail
    Person *p = head;
    while (p->number != k) {
        tail = p;
        p = p->next;
    }
    //从编号为k的人开始,只有符合p->next==p时,说明链表中除了p结点,所有编号都出列了,
    while (p->next != p)
    {
        //找到从p报数1开始,报m的人,并且还要知道数m-1的人的位置tail,方便做删除操作。
        for (int i = 1; i < m; i++)
        {
            tail = p;
            p = p->next;
        }
        tail->next = p->next;//从链表上将p结点摘下来,即报m的人出列。
        printf("出列人的编号为:%d\n", p->number);
        p = tail->next;//继续使用p指针指向出列编号的下一个编号,游戏继续
    }
    printf("出列人的编号为:%d\n", p->number);
    free(p);
}
//4.主函数
int main()
{
    printf("圆桌上一共有:");
    int PersonNum;
    scanf_s("%d", &PersonNum);
    Person* pHead = InitNodeLink(PersonNum);

    int k;
    printf("输入从第k人开始报数(k>1且k<%d):", PersonNum);
    scanf_s("%d", &k);
    int m;
    printf("输入数到m的人出列:");
    scanf_s("%d", &m);
    FindDelElemLink(pHead, k, m);
    return 0;
}

4.给定一个单链表,如何判断是否为有环链表?

  • 有环链表不一定是循环链表,可能是中间的位置出现环状;循环链表一定是首尾相连的。所以有环链表则指的是单链表中存在一个循环子链表。
  • 单链表:每一个结点的指针域只能指向一个结点。
  • 解析
  • 方法1
  • 实现思想:从链表的第一个节点开始遍历,每遍历至一个节点,都将其和所有的前驱节点进行比对,如果为同一个节点,则表明当前链表中有环;反之,如果遍历至链表最后一个节点,仍未找到相同的节点,则证明该链表中无环。
  • 时间复杂度:O(n^2)
  • 方法2
  • 实现思想:设置2个指针均指向链表头指针,让两个指针分别按照移动2个结点和1个结点的速度移动。当移动第一次,两者相差一个结点;第二次移动,则相差2次,依次类推,移动n-1次,就会有相差n-1个结点,如果存在环,则两指针重合,如果不重合,则是单链表中无环。
  • 时间复杂度为:O(n)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值