数据结构之手撕链表(讲解➕源代码)-1

简介: 数据结构之手撕链表(讲解➕源代码)

0.引言


我们在学习过顺序表之后,会发现两点不是很优秀的操作:

1.顺序表的头插和中间的插入,头删和中间的删除:

       需要不断的覆盖数据,时间复杂度是O(n),当我们的顺序表存入100w个数据的时候,花费的时间是非常之多的。

2.动态开辟空间:

       a. 一般动态开辟的空间都是以2倍的形式开辟,当我们已经开辟了100个空间,并且存满了,此时我们还需要存放5个数据,那么就又需要开辟200个空间了,我们存放5个数据之后,还剩余了195个空间没有放数据,这也就导致了空间的浪费。

       b.  而且我们开辟新空间,拷贝数据,释放旧空间还会有一定的消耗。


注意⚠️⚠️⚠️:

       我们在申请空间的时候必须要主动给它释放掉,因为申请空间的时候,是在堆上申请的,这段空间不会随着程序的结束而自然释放掉,所以要在我们程序结束之前,主动释放掉这段空间。


       那有没有一种结构会很简便呢?即不需要O(n)的时间复杂度,也解决了空间浪费的缺点,答案肯定是有的,就是我们本次要讲解的链表。

1.链表的概念

  链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的 。也就是说链表的物理结构不连续,但逻辑结构连续。


上面的是什么意思呢?博主用通俗的语言讲一下:

       我们先明确一点,链表的元素叫做节点,这里的链表,首先是一个线性表,需要存入数据的,顺序表是通过什么来找到下一个元素的呢?答案是数组下标,而链表是如果找到下一个节点的呢?是指针,链表是通过指针链接起来的。那我们链表主要的结构就出来了,元素和指向下一个元素的指针。如何将它们组合起来呢?就又用到了我们的结构体了。


       我们定义一个结构体来表示链表的节点,结构体内部要包括节点的值和指向下一个节点的指针。

      首先先了解如下概念


       前驱节点:一个节点前面的节点


       后继节点:一个节点后面的节点


       链表开始的节点称作:头节点


       链表末尾的节点称作:尾结点


       其余节点称作:中间节点。


       指向头节点的指针:头指针


       指向尾结点的指针:尾指针


       在知道这些之后,我们继续往下读,去了解链表的逻辑模型和物理模型吧!

2.链表的逻辑模型

       

       下图是链表的逻辑模型,黄色的正方形代表的是链表的节点,节点是由结构体组成的,而结构体里面有本节点的值指向下一个节点的指针。通过图片就把我们上面说的理论形象化了。


   从图中看,链表的逻辑模型一定是连续的,链表通过节点组成,节点之间通过指针链接的,是线性的。



3.链表的物理模型

 如图:在物理模型中,我们看到每个节点的地址都不是连续的,所以链表的物理模型不是连续的,不是线性的。我们所说物理上的线性是根据每个节点的物理地址来的,如果物理地址是挨着的就是线性,反之就不是。很显然,链表物理模型并不是线性的。


在学习完链表的逻辑模型和物理模型之后,就让我们一起看看链表具体有哪几种呢?

4.链表的分类

4.1 单向链表


单向链表是最简单的链表结构,但是在实现接口的时候却是最鸡肋的,具体实现会在后面提供源代码。

单向链表顾名思义,链表是单向的,1->2->3->4->5->NULL,缺点是某一个节点找不到它的前驱节点,只能找到后继节点,因为是单向的。

4.2 双向链表

       这个双向链表就比单向好很多了,通过一个节点可以找到前驱节点,也可以找到后继节点。


4.3 带头单向链表(带哨兵位)

       带头单向链表,这个带头是带一个哨兵位,方便咱们的头插和头删。

       哨兵位节点不存放元素。


4.4 带头双向链表(带哨兵位)


4.5 循环单链表


4.6 循环双向链表

4.7 带头循环单链表


4.8 带头循环双向链表

       这个结构是链表的最优结构,我们会后面的接口实现也提供了这个链表。


5. 单链表的实现(接口实现代码)

5.1 单链表的定义

       我们对单链表的定义是看节点需要什么,因为链表的节点是结构体构成的,节点需要什么就往结构体里存放对应的数据类型。

 单链表的每个节点需要该节点的值指向下一个节点的指针

       所以我们定义一个结构体,包括  值和指针 。

typedef int SLTDataType;
typedef struct SListNode
{
    SLTDataType data;
    struct SListNode* next;
}SLTNode;


5.3 单链表的动态申请一个节点空间

       这个函数是当我们需要往节点里存放值的时候必须经过的一步,在存放值之前,需要malloc一个节点的空间,开辟空间之后再把值放入,这里不要忘了将节点的next置为空,否则就是野指针。

SLTNode* BuySListNode(SLTDataType x) //创造新节点
{
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    if (newnode == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    newnode->data = x;
    newnode->next = NULL;
    return newnode;
}

5.4 单链表的销毁

       销毁这里就看创造了多少个节点的空间,挨个销毁就行。但是我们思考一个问题,当销毁了当前空间,那如何找到下一个节点呢?

如图:

  如果我们只是释放phead指向的节点,删除之后是无法找到下一个节点的,也就没办法继续释放后面的节点空间,那这里我们就需要一个指针over来指向phead指向的节点的下一个节点。



  这里我们就新建个指针over指向的是phead指向的节点的下一个节点,当释放当前节点之后,让phead指向over,over再指向phead指向节点的下一个节点就OK了。

void SLT_Destroy(SLTNode*phead)//销毁
{
    while (phead != NULL)
    {
        SLTNode *over = phead->next;
        free(phead);
        phead = over;
    }
}


5.5 单链表的头插头删

       头插:


 单链表的头插就比顺序表的头插快多了,不再需要覆盖数据,只需要将我们创建的新节点的next指向现在的头节点,再将指向头节点的指针指向新节点。但是需要注意的是我们改的是指针,如果想更改指针,就需要传入指针的地址,也就是用到了我们的二级指针。

头删:

       头删只需要删除当前节点,让下一个节点当新的头节点即可,这里需要用assert断言一下,assert函数在<assert.h>这个头文件里,是用来判断括号里的条件是否成立,成立就继续,不成立就报错。



 删除头节点之后,还要更新头节点,也需要一个指针 指向要删除的头节点的下一个节点,再将指向原头节点的指针指向新的头节点,这里也是更改指针,所以传入的也是二级指针。

void SLTPushFront(SLTNode** pphead, SLTDataType x) //头插
{
    SLTNode* newnode = BuySListNode(x);
    newnode->next = *pphead;
    *pphead = newnode;
}
void SLTPopFront(SLTNode** pphead) //头删
{
    // 空
    assert(*pphead);
    // 非空
    SLTNode* newhead = (*pphead)->next;
    free(*pphead);
    *pphead = newhead;
}


头删:

       头删只需要删除当前节点,让下一个节点当新的头节点即可,这里需要用assert断言一下,assert函数在<assert.h>这个头文件里,是用来判断括号里的条件是否成立,成立就继续,不成立就报错。


删除头节点之后,还要更新头节点,也需要一个指针 指向要删除的头节点的下一个节点,再将指向原头节点的指针指向新的头节点,这里也是更改指针,所以传入的也是二级指针。

void SLTPushFront(SLTNode** pphead, SLTDataType x) //头插
{
    SLTNode* newnode = BuySListNode(x);
    newnode->next = *pphead;
    *pphead = newnode;
}
void SLTPopFront(SLTNode** pphead) //头删
{
    // 空
    assert(*pphead);
    // 非空
    SLTNode* newhead = (*pphead)->next;
    free(*pphead);
    *pphead = newhead;
}


5.6 单链表的尾插尾删

尾插:

       尾插这里要分两种情况:

       1.没有节点的尾插:


这里的插入的节点就是头节点,需要更改一下指向头节点的指针phead,更改指针就用到二级指针。

       

       2.有节点的尾插:

  对于有节点的尾插,我们首先应该找到链表的尾结点,再将尾结点的next指向新节点。

尾删:

       尾删分三种情况:


 1.链表为空:

       这里就需要用assert断言一下,为空就报错。

       2.链表只剩一个节点:


当链表只有一个节点的时候,也就是*pphead->next == NULL。改变的就是头节点,因为我们对头节点进行删除,链表就没有节点了,需要置为空,所以头指针就要等于空指针,更改了指针,就用到了二级指针。


e022e745ea8a43a5b227fce9a459192c.png


       3.链表还有两个及以上的节点:

af322b76250b4980ad75516f5ceb74bf.png

     


void SLTPushBack(SLTNode** pphead, SLTDataType x) //尾插
{
    SLTNode* newnode = BuySListNode(x);
    if (*pphead == NULL)
    {
        // 改变的结构体的指针,所以要用二级指针
        *pphead = newnode;
    }
    else
    {
        SLTNode* tail = *pphead;
        while (tail->next != NULL)
        {
            tail = tail->next;
        }
        // 改变的结构体,用结构体的指针即可
        tail->next = newnode;
    }
}
void SLTPopBack(SLTNode** pphead) //尾删
{
    // 1、空
    assert(*pphead);
    // 2、一个节点
    // 3、一个以上节点
    if ((*pphead)->next == NULL)
    {
        free(*pphead);
        *pphead = NULL;
    }
    else
    {
        SLTNode* tail = *pphead;
        while (tail->next->next)
        {
            tail = tail->next;
        }
        free(tail->next);
        tail->next = NULL;
    }
}

5.7 单链表的在pos位置之前的插入

       这里需要判断pos所在位置是否是头节点,如果是头节点就是头插;如果不是就正常的在pos位置之前插入就可以了

       

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//pos之前插入
{
  assert(pphead);
  assert(pos);
  if (pos == *pphead)
  {
    SLTPushFront(pphead, x);
  }
  else
  {
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    SLTNode* newnode = BuySListNode(x);
    prev->next = newnode;
    newnode->next = pos;
  }
}


5.8 单链表的在pos位置之后的插入

void SLTInsertAfter(SLTNode* pos, SLTDataType x)//在pos之后插入
{
  assert(pos);
  SLTNode* newnode = BuySListNode(x);
  pos->next = newnode;
  newnode->next = pos->next;
}

5.9 单链表的删除pos位置的节点

       如果pos是头,就需要头删

       如果pos不是头,就正常删除。

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
  assert(pphead);
  assert(pos);
  if (pos == *pphead)
  {
    SLTPopFront(pphead);
  }
  else
  {
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
    //pos = NULL;
  }
}


5.10 单链表的删除pos位置之后的节点

void SLTEraseAfter(SLTNode* pos)
{
  assert(pos);
  // 检查pos是否是尾节点
  assert(pos->next);
  SLTNode* posNext = pos->next;
  pos->next = posNext->next;
  free(posNext);
  posNext = NULL;
}

5.11 单链表的查找

SLTNode* SLTFind(SLTNode* phead, SLTDataType x) //查找
{
  SLTNode* cur = phead;
  while (cur)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}


相关文章
|
14天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
42 4
|
15天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
15天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
1月前
|
存储 Java
数据结构第三篇【链表的相关知识点一及在线OJ习题】
数据结构第三篇【链表的相关知识点一及在线OJ习题】
25 7
|
1月前
|
算法 Java
数据结构与算法学习五:双链表的增、删、改、查
双链表的增、删、改、查操作及其Java实现,并通过实例演示了双向链表的优势和应用。
16 0
数据结构与算法学习五:双链表的增、删、改、查
|
14天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
33 0
|
28天前
|
存储
[数据结构] -- 双向循环链表
[数据结构] -- 双向循环链表
19 0
|
1月前
|
存储
探索数据结构:便捷的双向链表
探索数据结构:便捷的双向链表
|
1月前
|
存储
探索数据结构:单链表的实践和应用
探索数据结构:单链表的实践和应用
|
1月前
|
算法 Java
数据结构与算法学习六:单向环形链表应用实例的约瑟夫环问题
这篇文章通过单向环形链表的应用实例,详细讲解了约瑟夫环问题的解决方案,并提供了Java代码实现。
19 0