【数据结构】九、链表、双向链表和双向循环链表

目录

一、什么是链表

二、结构体定义

三、链表的基本操作

四、关于指针和指向指针的指针

五、对链表效率的提高

1、快速获取链表的节点个数

2、快速在链表末尾添加节点

六、关于双向链表和循环链表

1、循环链表

2、双向链表


一、什么是链表

链表是一种常见的数据结构,由数据域和指针域组成,在内存中的地址是非连续的,通过指针域就能够依次访问所有节点。相较于数组而言,链表可以高效地添加和删除元素,并且可以根据需要开辟和释放内存空间,因此其对空间的利用非常高效。

链表分为普通链表、双向链表和双向循环链表,本章节我将会主要对链表操作的代码进行讲解,以及一些遇到的问题。

二、结构体定义

链表的结构体的基本成分为数据域和指针域,定义如下:

typedef struct LIST {
    int num;           //数据域
    struct LIST* next; //指针域
}DATA;

三、链表的基本操作

在代码示例中,我创建了一个头节点,但并不用来存放数据,只是将其作为查找后面的节点的线索,这样做的好处一是我们的头节点永远不会改变,即使是删除了链表存储的第一个元素,其次是对于参数的类型更加统一化(第四部分有说明)。

 /* @brief 初始化头指针
 *  @param  链表头指针的地址
 *  @return None
 */
void List_Init(DATA** head)//对表头进行操作,需要传入指针的指针,其他修改只是通过指针修改对应内存的值
{
    *head = (DATA*)malloc(sizeof(DATA));
    (*head)->num = 0;
    (*head)->next = NULL;
}

/* @brief 计算链表长度
*  @param  链表头指针
*  @return 链表长度
*/
int List_Length(DATA* head)
{
    int len = 0;
    for (DATA* i = head->next; i != NULL; i = i->next) len++;
    return len;
}
//对于此功能,我们也可以在结构体中多添加一个size数据,用来存放元素个数,这样一来就不需要遍历整个链表来计算大小了

/* @brief 在第n个节点前面插入
*  @param  链表头指针
*  @param  插入的位置
*  @prarm  插入的数据
*  @return 插入成功返回1,失败返回0;
*/
int List_Insert_Front(DATA* head, int n, int data)
{
    int ret = 0;   //记录是否成功标志位
    if (n > List_Length(head)) return ret;

    DATA* p = head;
    DATA* x = (DATA*)malloc(sizeof(DATA));
    x->num = data;
    x->next = NULL;

    while (--n)
    {
        p = p->next;
    }
    x->next = p->next;
    p->next = x;
    ret = 1;
    return ret;
}

/* @brief 在数据为a的节点后面插入一个新节点,如果没有a值,则把节点插入到末尾
*  @param  链表头指针
*  @param  新插入的节点
*  @param  a的值(如果省略,则直接在末尾插入)(如果无法查找到数据a,在末尾插入)
*  @return None
*/
void List_Insert_Last(DATA* p, DATA* x, int a)
{

    while (p->next != NULL)
    {
        if (p->num == a)
        {
            x->next = p->next;
            p->next = x;
            return;
        }
        p = p->next;
    }
    p->next = x;
    return;
}

/* @brief 打印链表中的所有数据
*  @param  链表头指针
*  @return None
*/
void List_Print(DATA* head)
{
    for (DATA* i = head->next; i != NULL; i = i->next)
    {
        printf("%d ", i->num);
    }
    printf("\n");
}

/* @brief 查找指定位置的数据
*  @param  链表头指针
*  @param  链表的位置
*  @param  缓存
*  @return 查找成功返回1,否则返回0
*/
int List_Find(DATA* p, int n, int* x)
{
    if (n > List_Length(p)) return 0;
    while (n--) p = p->next;
    *x = p->num;
    return 1;
}

/* @brief 删除指定位置的节点
*  @param  链表头指针,要删除的节点
*  @return 删除成功返回1,否则返回0
*/
int List_Delete(DATA* head, int n)
{
    if (head->next == NULL) return 0;
    if (n > List_Length(head)) return 0;

    DATA* p = head, * pnext = p->next;

    while (--n && pnext->next != NULL)
    {
        pnext = pnext->next;
        p = p->next;
        //注意p = p->next 和 p->next = pnext 的区别!!前者只是将指针重定向,而后者会直接改变头指针,而这里p = pnext会产生逻辑错误
    }
    p->next = pnext->next;

    free(pnext);
    return 1;
}

/* @brief 删除所有节点
*  @param  链表头指针
*  @return None
*/
void List_Distory(DATA* head)
{
    DATA* pnext = head->next;
    if (pnext == NULL) return;

    DATA* dep = NULL;

    while (pnext->next != NULL)
    {
        dep = pnext;
        pnext = pnext->next;
        free(dep);
    }
    free(pnext);
    head->next = NULL;

    return;
}

四、关于指针和指向指针的指针

对于上面的代码我们发现,只有初始化头节点的函数参数是指向指针的指针,而其他函数都是调用链表头的指针,这其中有什么区别呢?

首先,我们在创建一个链表时并不是直接 struct LIST head 的,而是创建一个指向链表头的指针,为了保证数据类型相同,所以传入指针。

而初始化头指针时由于需要对该数据直接进行操作,改变其数据,因此传入指针的指针。

但是添加节点的操作不是也对数据类型进行改变了?没错,是改变了,但是改变的不是头指针指向节点的位置,(这就体现了头指针不存放数据的好处了),我们只是需要通过链表头的指针域来访问后面的节点,从而对后面的节点进行操作,而无论是通过指针访问还是通过指针的指针解引用,都能达到这个目的,因此对链表的操作无论是传入链表的头指针还是传入链表头指针的指针都是可行的,并且前者更加方便可观

五、对链表效率的提高

1、快速获取链表的节点个数

对于上面的代码我们通过遍历来获取链表的节点个数,其时间复杂度是O(n)

int List_Length(DATA* head)
{
    int len = 0;
    for (DATA* i = head->next; i != NULL; i = i->next) len++;
    return len;
}

优化:在链表结构体中增加一个size变量,在每次增加删除节点时改变size的值,这样就可以直接访问得到节点个数,时间复杂度为O(1)

2、快速在链表末尾添加节点

上述代码中我们还是通过遍历来找到尾节点,时间复杂度为O(n)

优化:新建立一个结构体来存放链表的头和尾节点的指针:

typedef struct ListPtr{
  DATA* head;
  DATA* rear;
}ListPtr_t;

这样在添加节点时,直接传入尾节点指针的指针即可(直接改变了数据)时间复杂度为O(1)

void List_Insert(ListPer** rear,DATA* new)
{
  (*rear)->next = new;
}

六、关于双向链表和循环链表

1、循环链表

循环链表就是把链表的头和尾直接连接起来,即链表尾节点的指针域不再是空,而是指向头节点,(由于我的代码中头节点不存放数据,因此实际上是指向头节点的下一个位置),然而单独使用循环链表是没有用的,经常需要搭配双向链表来使用。

2、双向链表

在普通链表中,当我们需要实现遍历到某个节点然后寻找该节点的上一个节点的值时,需要重新再遍历一次,因为我们以及遗忘了该节点的上一个节点的指针,而通过双向链表就可以很容易的访问到该节点。

//结构体定义
typedef struct LIST {
    int num;
    struct LIST* next;
    struct LIST* last;
}DATA;

如果是双向循环链表,也可以实现通过头指针直接找到尾指针的节点。

但是在很多时候,只需要多创建一个节点来存储上一个节点的地址就可以了,不需要使用到双向链表,比如上面的删除节点的函数,其逻辑如下:

当然,使用什么类型的链表都需要根据实际应用来选取,毕竟代码是死的人是活的嘛!

最后感谢你观看完我的文章,如果文章对你有帮助,可以点赞收藏评论,这是对作者最好的鼓励!不胜感激🥰

  • 30
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值