手撕单链表

前面我们已经学过了顺序表,现在我们着手来学习一下难度升高那么一点点的单链表。

所谓“链”就像是有链条一样连起来,这样会不会让大家想象到火车呢?就算没坐过火车地铁总坐过吧,每一节车厢是不是都由链条一样把彼此串联起来?那么单链表也是这样,每一节车厢我们称它们为结点,结点中有两个元素,一个是自己存储的值,一个是指向下一个结点的指针,所以,我们在定义单链表的时候,需同时包含两个元素,所以,就需要用到结构体变量。

在这里插入图片描述
需强调的一点是,正因为每节车厢都是靠链条连接在一起,所以每节车厢都是独立存在的,即每个结点都是独立存在的,只是结点中有一个成员有指向下一个结点的指针,使得车厢与车厢之间有了联系。

单链表结构的定义

在这里插入图片描述
假如我们下一个结点的地址为0x100呢?那我们给大家表示一下
在这里插入图片描述
值得一提的是,链表的最后一个结点的next指针一定为空指针,那么我们怎么对单链表进行初始化呢?

单链表的初始化

假如我们现在想要创建一个值为5的结点呢?即下图
在这里插入图片描述
那么我们就要开辟一个空间来存放这些元素,链表的初始化我们都是让他的next指针指向NULL的,后期才改变next的指向使彼此建立联系
我们上面已经定义了链表的结构,上代码强化一下

typedef int Slistdatatype;
typedef struct Slistnode
{
     Slistdatatype data;//结点里面存放的数值,想改变存放数值只需改变把上面的int改成想要的类型即可
     struct Slistnode * next;//指向下一个结点的指针
}Slist;//将struct Slistdatatype 重命名为Slist了

Slist * buynode(Slistdatatype x);//x即为我们想要在结点的data元素存储的值
{
     Slist * tmp = (Slist *)malloc(sizeof(Slist));
     if(tmp == NULL)
     {
        return NULL;
     }
     //能走到这里,说明tmp不是空指针
     Slist * newnode = tmp;
     newnode->data = x;//让此结点存放的数值为5
     newnode->next = NULL;//被创建的新的结点的next指针一定为NULL,后续再进行更改
     return newnode;//返回指向创建好的新结点的指针
}

为什么要定义tmp指针?因为malloc函数在申请空间的时候可能会申请失败,申请失败会导致指针变成野指针,所以我们要定义tmp指针,如果其不等于NULL,再让newnode去接收

单链表的尾插

前面讲完了怎么创建结点,现在我们就让结点之间产生联系

int main()
{
   Slist * node1 = buynode(1);//让node1指向存放着1数据的结点
   Slist * node2 = buynode(2);//让node2指向存放着2数据的结点
   node1->next  = node2;//产生联系
   return 0;
}

在这里插入图片描述

现在我们让node1和node2进行了关联,现在如果我们想要在2后面插入一个存有数值为3的结点呢?(也就是尾插)

我们得先调用buynode函数创建一个新结点,并返回指向该结点的指针,再让node2的next指针指向新结点

那我们的目的就是要先找到原先的尾结点(也就是上图指向2的这个结点),所以我们应先拿来指向首结点的指针,再让其遍历(指针不为空,就往后走,也就是让指针指向自己的next指针),接下来,我们上代码示例。

void Slistpushback(Slist ** pphead,Slistdatatype x )
{
    if(*pphead == NULL)
    {
       *pphead = buynode(x);//这是首结点为空的情况,直接让其指向新创建的结点
    }
    //来到这里,就是首结点不为空的情况,现在要让指针指向next指针为NULL的结点(也就是尾结点),接下来,看我操作
    Slist * pur = *pphead;
    while(pur->next!=NULL//pur的next指针不为空就进入循环,跳出循环时,pur的next指针就指向了NULL
    {
       pur = pur->next;//让pur指针往后走
    }
    //跳出循环,此时pur指针指向了尾结点
    pur->next = buynode(x);//这样,就实现了我们的尾插操作
}

单链表的头插

讲完了尾插,我们来讲讲头插
头插就比较简单,思路就是:新结点的指针的next指针指向我们原先的头结点
在这里插入图片描述
所以先让我们新结点的next指针指向原先的头部指针*pphead,再把原先的头结点指向newnode即可
上代码

void Slistpushfront(Slist ** pphead,Slistdatatype x)
{
    Slist * newnode = buynode(x);//x即为新结点data里面存放的数据,上图为3
    newnode->next = *pphead;//新结点的next指针指向了原先的头结点
    *pphead = newnode;//再让原先的头结点指向新创建的结点,相当于新创建的结点为新的头结点
}

单链表的尾删

简单来讲就一句话:把尾结点释放掉,让尾结点的前一个结点的next指针指向NULL;
我画图帮助大家理解
在这里插入图片描述
那,让我们上代码实现一下吧,我们需要定义两个指针,一个指向尾结点,一个要指向尾结点的前一个结点,目的是释放完尾结点的空间后,可以直接让尾结点的前一个结点的next指针指向NULL

void Slistpopback(Slist ** phead)
{
    if((*pphead)->next == NULL)//此为只有一个结点的情况
    {
       free(*pphead);//直接释放掉
       *pphead = NULL;//释放完要及时置为空指针
    }
    else
    {
       Slist * head;//定义一个指针,它的目的是要指向尾结点
       Slist * prev;//定义第二个指针,目的是指向尾结点的前一个结点
       while(head->next != NULL)
       {
           prev = head;
           head = head->next;//让head往后走,prev跟着head,并且确保prev比head少走一步,所以这两句代码的顺序不能颠倒
       }
       //出了这个循环,head就指向了尾结点,prev则指向head的前一个
    }
    free(head);
    prev->next = NULL;
    
}

单链表的头删

单链表的头删思路:将指向头结点的下一个结点的指针保存下来,然后把头结点释放掉,再让头结点指向保存的那个结点

void Slistpopfront(Slist ** pphead)//这里需要传二级指针,因为原先指向头结点的指针plist现在已经不指向头(因为被删除了),所以plist要指向头结点的下一个结点
{
   Slist * pur = *pphead->next;//定义一个相同类型的指针,让其走向下一个结点
   free(*pphead);//原先指向的头结点被释放
   *pphead = pur;//让*pphead指向保存好的指向下一个结点
}

单链表的查找

我们都知道,在单链表中,每个结点都是独立存在的,只是因为其内部有一个指向下一个结点的指针才让彼此之间有了联系。【每一个结点中都有两个元素,存储的值以及存放指向下一个结点的地址】,那么比如单链表中,第一个结点存放了1,第二个存放了2,第三个结点存放了3,我们要找到指向这个存放数字2的结点的指针该怎么办呢?就得利用到它们彼此之间存放值不同的特性了(即newnode->data),如果newnode->data是我们想要找到的数字,那么返回newnode这个指针即可,上图。
在这里插入图片描述

接下来,让我们用代码实现一下,注意前面的返回值是void,也就是不返回任何值,这次我们要返回指针,也就是Slist

Slist * find(Slist * phead,Slistdatatype x)//x为我们需要查找的值,这里传一级指针是因为不会改变原链表的结构,只是单纯返回某一个结点的指针
{
    Slist * pur = phead;
    while(pur->data != x&&pur != NULL)//指针的data元素如果不是我们想要的x的话,那就进入循环;相反,如果指针的data元素为x的话,刚好跳出循环,那么此时的pur指针刚好就是我们想要找的指针
    {
       pur = pur->next;//让pur往后走
    }
    return pur;
}

如上面代码所示,如果pur的data元素是x的话,那么就返回pur,如果不是,那就返回NULL

单链表在指定的指针后面插入新结点

这里的指定指针就是我们上面所显示的案例,例如我们想要在指向2这个结点的指针(假设为node2)后面插入一个新的结点。那就得找到node2,然后再对其进行操纵

void pushfindafter(Slist * pos,Slistdatatype x)
{
   Slist * pur = pos;//pur也指向2这个结点
   Slist * next = pur->next;//用next指针保存原先2这个结点的下一个结点
   Slist * newnode = buynode(x);
   newnode->next = next;
   pur->next = newnode;//
}
int main()
{
   Slist * find = find(pphead,2);//假设现在单链表从左向右一次存放着1 2 3,pphead就是指向1结点的指针
   pushfindafter(find,100);
   return 0
}

在这里我们已经给了一个示例教大家如何在指定指针后面插入新的结点(如pos指针),在这里大家可以试着完成删除pos指向的结点以及删除pos后一个的结点。至于为什么不进行删除pos指向结点的前一个结点呢?

那是因为,单链表的next指针指向的是下一个结点的指针,其没法通过该结点找到上一个的结点,所以给你一个pos指针,你只能对pos指针往后的结点进行插入以及删除操作。如果给你的是指向头结点的指针和pos结点,那你就可以对目前的单链表进行任何操作,(因为你可以借着头结点去找到任何位置的结点)。

单链表的删除

As we all know,链表中的一个个的结点通过动态内存函数(如malloc,calloc,realloc)申请内存的,那么我们在申请这些空间的时候,程序运行结束后,要及时的释放掉这些内存。

以下是关于释放单链表的深度解析

释放单链表就是把每一个指向该结点的指针free掉,也就是free(指针),那么就会带来一个问题,如果我只给你头结点的话你把指向头结点的指针释放了,那么就无法找到指向头结点后面结点的指针了。所以,我们需要定义两个指针【一个在前面跑,一个在后面被释放,再让被释放的指向在前面跑的指针,再让前面跑的再往前跑】

我们上代码实测:

void destory(Slist ** pphead)
{
   Slist * pur = *pphead;
   while(pur != NULL)
   { 
      Slist * next = pur->next;//指向pur的下一个结点
      free(pur);//释放pur所指向的结点
      pur = next;//把pur指向next指针指向的结点
   }
}

完结撒花

### 常见面试代码题目及解答 #### 1. 最长回文子串 (LeetCode 5) 动态规划可以用来解决这个问题。定义 `dp[i][j]` 表示从索引 i 到 j 的子串是否为回文[^1]。 ```python def longest_palindrome(s: str) -> str: n = len(s) dp = [[False]*n for _ in range(n)] max_len, start = 1, 0 for end in range(n): for begin in range(end+1): if s[begin] == s[end] and (end-begin <=2 or dp[begin+1][end-1]): dp[begin][end] = True cur_len = end - begin +1 if cur_len > max_len: max_len = cur_len start = begin return s[start:start+max_len] ``` #### 2. 接雨水 (LeetCode 42) 利用双指针法,分别记录左右两侧的最大高度,并计算当前柱子能接多少水。 ```python def trap(heights: list[int]) -> int: left, right = 0, len(heights)-1 l_max = r_max = water = 0 while left < right: if heights[left] < heights[right]: l_max = max(l_max, heights[left]) water += l_max -heights[left] left +=1 else: r_max = max(r_max, heights[right]) water +=r_max-heights[right] right -=1 return water ``` #### 3. 实现类似 ArrayList 的自动扩容数组 当容量不足时,创建一个新的更大的数组并将原数据复制过去。 ```java public class MyArrayList<T> { private Object[] data; private int size; public MyArrayList(int initialCapacity){ this.data=new Object[initialCapacity]; this.size=0; } public void add(T element){ ensureCapacity(); data[size++]=element; } @SuppressWarnings("unchecked") public T get(int index){ return (T)data[index]; } } ``` #### 4. 单链表反转 通过迭代的方式逐个改变节点指向的方向来完成链表的反转。 ```c++ ListNode* reverseList(ListNode* head) { ListNode *prev=nullptr,*curr=head; while(curr!=nullptr){ ListNode* nextTemp= curr->next; curr->next= prev; prev= curr; curr= nextTemp; } return prev; } ``` #### 5. LRU 缓存机制 (LeetCode 146) 使用哈希表存储键值对映射关系以及双向链表维护最近使用的顺序。 ```cpp class LRUCache{ private: struct Node{int key,val;}; }; ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值