数据结构记录-链表

1、单链表

1、单链表的组成

最基本的单链表组成如下:

typedef struct Link
{
    char elem; /*数据域*/
    struct Link *next; /*指针域*/
}link;/*节点名,每个阶段都是一个Link结构体*/

为什么这样的就是链表呢,需要从这个结构体内部组成来看,struct Link next; 定义了一个指针变量 next,它的类型是 struct Link,即指向 struct Link 结构体的指针,所以它可以执行一个这样的结构体,所以可以指向下一个结构体,只要我们每次都指一下就会这样一直循环不断下去,这样就形成一个链表。

2、单链表实现

创建一个最简单的单链表如下所示:

link *initLink() 
{
    /*创建无头结点*/
    // link *p = NULL;
    // link *temp = (link*)malloc(sizeof(link));

    // temp->elem = 1;
    // temp->next = NULL;

    // p = temp;

    // for(int i = 2; i<5; i++)
    // {
    //     link *a = (link*)malloc(sizeof(link));
    //     a->elem = i;
    //     a->next = NULL;

    //     temp->next = a;
    //     temp = temp->next; /*这样循环创建后驱*/
    // }

    /*创建有头结点*/

    link *p = (link*)malloc(sizeof(link));
    link *temp = p; /*声明一个指针指向头结点*/

    for(int i = 1; i<5; i++)
    {
        link *a = (link*)malloc(sizeof(link));
        a->elem = i;
        a->next = NULL;

        temp->next = a;
        temp = temp->next; /*这样循环创建后驱*/
    }

    return p;
}

最关键的代码在这:
temp->next = a;
temp = temp->next;

从循环来看,第一次循环,数据域赋值为1,temp的第一个next指向这个新申请的a,之后temp更新到a这里,之后继续创建数据域为2的,在执行相同操作,这样一直到4结束。

但是这样链表的第一个元素,数据域是空的,因为没有地方给他赋值,图示上是这样的
image

3、单链表的基本操作

插入元素

插入如下图所示,就是先移动到他前面一个节点,之后修改next指针的指向。
image

/*
插入元素:
1、将新节点的next指针指向插入位置后的节点
2、将插入位置前节点的next指针指向插入节点

[param] elem 新数据
[param] add 插入位置
*/
link *insertElem(link *p, int elem, int add)
{
    link *temp = p;

    for(int i = 1; i<add; i++) /*将temp指针指向要插入的前一个节点*/
    {
        temp = temp->next;
        if(temp == NULL)
        {
            printf("插入位置无效\n");
            return p;
        }
    }

    link *c = (link*)malloc(sizeof(link));
    c->elem = elem;
    c->next = temp->next; /*创建一个新节点,将c的next指向temp的next,再把temp的next指向c*/
    temp->next = c;
    return p;
}

删除元素

和插入类似,找到,之后指向下一个的下一个。

/*
删除元素:
1、将节点从链表中摘除
2、手动释放节点

[param] elem 要删除元素的值
*/
link *delElem(link *p, int add)
{
    link *temp = p;
    for(int i=1;i<add;i++) /*移动到要删除的上一个节点*/
    {
        temp = temp->next;
        if(temp->next == NULL)
        {
            printf("删除位置无效\n");
            return p;
        }
    }
    link *del = temp->next;
    temp->next = temp->next->next; /*指向下一个节点是下一个的下一个*/
    free(del);
    return p;
}

寻找元素

int selectElem(link *p, int elem)
{
    link *t = p;

    int i=1;
    while (t->next)
    {
        t=t->next;
        if(t->elem == elem)
            return i;
        i++;
    }
    return -1;
}

link *amendElem(link *p, int add, int newElem)
{
    link *temp = p;
    temp = temp->next;
    for(int i=1; i<add;i++)
    {
        temp = temp->next;
    }
    temp->elem = newElem;
    return p;
}

修改元素

link *amendElem(link *p, int add, int newElem)
{
    link *temp = p;
    temp = temp->next;
    for(int i=1; i<add;i++)
    {
        temp = temp->next;
    }
    temp->elem = newElem;
    return p;
}

2、顺序表和链表优缺点总结

之前总结了顺序表:顺序表示例

这两种储存结构的差异在于顺序表申请的是一大块完整的空间,而链表申请的是不一定是完整的,是有什么时候需要就申请空间,因此从空间利用率来看,顺序表的空间利用率要比链表更高一点。

从时间复杂度来看,从以下两类来看:

  • 1、涉及访问元素的操作,元素的插入、删除和移动操作极少,适合使用顺序表。这是因为,顺序表中存储的元素可以使用数组下标直接访问,无需遍历整个表,因此使用顺序表访问元素的时间复杂度为O(1);而在链表中访问数据元素,需要从表头依次遍历,直到找到指定节点,花费的时间复杂度为O(n);
  • 2、涉及元素的插入、删除和移动,访问元素的需求很少,适合使用链表。链表中数据元素之间的逻辑关系靠的是节点之间的指针,当需要在链表中某处插入或删除节点时,只需改变相应节点的指针指向即可,无需大量移动元素,因此链表中插入、删除或移动数据所耗费的时间复杂度为O(1);而顺序表中,插入、删除和移动数据可能会牵涉到大量元素的整体移动, 因此时间复杂度至少为O(n);

3、单链表反转

常用的实现方案有 4 种,这里分别将它们称为迭代反转法、递归反转法、就地逆置法和头插法。递归反转法更适用于反转不带头节点的链表;其它 3 种方法既能反转不带头节点的链表,也能反转带头节点的链表。

1、迭代反转

迭代反转链表的思路如下所示:
image

代码如下:

link *iteration_reverse(link *head)
{
    if(head == NULL || head->next == NULL){
        printf("link error \n");
        return head;
    }
    else{
        link *beg = NULL;
        link *mid = head;
        link *end = head->next;
        
        while(1)
        {
            mid->next = beg;
            if(end == NULL)
                break;
            
            beg = mid;
            mid = end;
            end = end->next;
        }

        /*修改head头指针的指向*/
        head = mid;
        return head;
    }
}

2、递归反转

流程如下:(这个没怎么看懂,要debug看一下)
image

源代码如下:

link *recursive_reverse(link *head)
{
    if(head == NULL || head->next == NULL)
        return head;
    else{
        link *new_head = recursive_reverse(head->next);

        head->next->next = head;
        head->next = NULL;
        return new_head;
    }
}

3、头插法反转

原理图如下所示:
image

代码如下:

link *head_reverse(link *head)
{
    link *new_head = NULL;
    link *temp = NULL;
    if(head == NULL || head->next == NULL)
        return head;
    while (head != NULL)
    {
        temp = head;
        head = head->next; //将temp从head中摘除

        temp->next = new_head; /*temp插入到new_head的头部*/
        new_head = temp;
    }
    return new_head;
}

4、就地逆置法反转

原理如下图所示:
image

代码如下

link *local_reverse(link *head)
{
    link *beg = NULL;
    link *end = NULL;

    if(head == NULL || head->next == NULL)
        return head;
    
    beg = head;
    end = head->next; /*暂存一下临时的节点*/
    while (end != NULL)
    {
        beg->next = end->next; /*移除end*/

        /*将end移动到表头*/
        end->next = end;
        head = end;
        /*end指向beg的后一个节点*/
        end = beg->next;
    }
    return head;
}
posted @   LX2020  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
点击右上角即可分享
微信分享提示