【数据结构】剖析链表10大经典问题


🍃 如果觉得本系列文章内容还不错,欢迎订阅🚩
🎊个人主页:小编的个人主页
🎀 🎉欢迎大家点赞👍收藏⭐文章
✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍

🐼前言

💫在之前小编给大家介绍了单链表双链表,今天我们来实战关于力扣和牛客网上一些常见链表算法:

🐼 移除链表元素

移除链表元素

🔎题目描述

💫给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
在这里插入图片描述
示例 2:

输入:head = [], val = 1 输出:[]
示例 3:

输入:head = [7,7,7,7], val = 7 输出:[]

📋思路分析

🐸遍历原链表,创建新链表,如果不是要删除的元素,则尾插到新链表。

typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    ListNode* pcur = head;
    ListNode *newhead, *newtail;
    newhead = newtail = NULL;
    while (pcur) {
        if (pcur->val != val) {
            // 链表为空
            if (newhead == NULL)
                newhead = newtail = pcur;
            else {
                // 链表不为空

                // 尾插到新链表
                newtail->next = pcur;
                newtail = newtail->next;
            }
        }
        pcur = pcur->next;
    }
    // 将新链表尾节点置为空,防止尾结点还有最后一个未删除元素
    if (newtail)
        newtail->next = NULL;
    return newhead;
}

🌻代码解析

💫创建新链表头指针,尾指针,让pcur遍历链表。因为链表不带头,这里需要分情况,如果链表为空,让pcur给newhead和newtail;否则,尾插到新链表。最后,更新pcur.
💫对边界情况的处理,就像题目的例子。将5拿下来尾插,但是5节点后面紧跟着6,所以,将newtail的next指针置为空,防止这种情况的发生。

💫该题时间复杂度为O(N),遍历一遍链表,空间复杂度为O(1),只创建有效个变量
🍂画图剖析:
在这里插入图片描述

🐼反转链表

反转链表

🔎题目描述:

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例1:
在这里插入图片描述
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例2:
在这里插入图片描述
输入:head = [1,2]
输出:[2,1]
示例3:
输入:head = []
输出:[]

📋思路分析

🐸思路1:创建新链表,将原链表拿下来头插。

typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) 
{
    ListNode* pcur = head;
    ListNode* newHead = NULL;

    //头插
    while(pcur)
    {
       ListNode* next = pcur->next;
       pcur->next = newHead;
       newHead = pcur;
       pcur = next;
    }
    //走到头
    return newHead;
}

🌻代码解析

💫pcur 遍历原链表,创建新链表,保存原链表的下一个节点next,每头插一次,将pcur走到next。最后将新的链表头返回。
💫该题时间复杂度为O(N),遍历一遍链表,空间复杂度为O(1),只创建有效个变量

🐸思路2:创建三指针,刚开始让n1指向NULL,n2指向要反转的节点(刚开始n2指向头),n3保存下一个节点(n2->next),接着让n2的后继指针(next)指向前一个节点(n1),n1走到n2的位置,n2走到n3的位置,n3走到n2的下一个位置

typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
    ListNode* n1,*n2,*n3;
    if(head == NULL)
        return head;
    
    n1 = NULL,n2 = head,n3 = n2->next;
    while(n2)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n3)
            n3 = n3->next;
    }
    //n2走到NULL
    return n1;
}

🌻代码解析

💫反转n2的指向,让n2指向n1.完成反转。接着,为了能完成下一次反转,让n1走到要反转的前一个节点n2,n2走到下一个要反转的节点n3,只要n3不为空,n3保存n2的下一个节点。直到要反转的节点为空,最后n1就是头结点。直接返回。注意边界处理,如果链表为空链表,直接返回
💫时间复杂度为O(N),遍历一遍链表,空间复杂度为O(1),只创建有效个变量
🍂画图剖析:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

🐼合并两个有序链表

合并两个有序链表
🔎题目描述:

💫将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
在这里插入图片描述
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]

📋思路分析

🐸 创建新链表。分别用l1,l2遍历两个链表,通弄比较l1,l2节点的值,将较小的值先尾插到新链表,并向后走一步(比如:l1->val>l2->val,将l2尾插到新链表,l2向后走一步,l1不动)

typedef struct ListNode ListNode; 
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
      //判空
    if(list1 == NULL)
        return list2;
    if(list2 == NULL)
        return list1;
    
    ListNode* l1 = list1;
    ListNode* l2 = list2;
    //
    // ListNode* newhead = NULL;
    // ListNode* newtail = NULL;
    ListNode*newhead,*newtail;
    newhead = newtail = (ListNode*)malloc(sizeof(ListNode));

    while (l1 && l2)
    {
            //创建新链表//尾插l1
        if((l1->val)<(l2->val))
        {
            newtail->next = l1;
            newtail = newtail->next;
            l1 = l1->next;
        }
        //尾插l2
        else
        {
            newtail->next = l2;
            newtail = newtail->next;
          l2 = l2->next;
        }
    }
    if(l1)
    {
        newtail->next = l1;
    }
    if(l2)
    {
        newtail->next = l2;
    }
    //释放
    ListNode* ret = newhead->next;
    free(newhead);
    newhead = NULL;
    return ret;
}

🌻代码解析

💫 用l1,l2分别遍历两个原链表,比较节点值的要求,尾插到带头新链表。最后,如果l1还没走完,将l1尾接到新链表。如果l2还没走完,将l2尾接到新链表。保存头结点的下一个节点以方便返回,释放带头节点。💫注意对边界情况的处理,如果l1为空链表,返回l2,如果l2为空链表,返回l1,
💫该题时间复杂度为O(N),遍历一遍链表,空间复杂度为O(1),只创建有效个变量
🍂画图剖析:
在这里插入图片描述

🐼CM11 链表分割

链表分割
🔎题目描述:

描述
现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。

📋思路分析

🐸创建大小链表。遍历原链表,将小于x的值尾插到小链表中,将大于x的值尾插到大链表中。最后将小链表与大链表相连。

#include <cmath>
#include <functional>
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
         ListNode* lesshead,*greaterhead,*lesstail,*greatertail;
        lesstail = lesshead =(ListNode*)malloc(sizeof(ListNode));

        greatertail = greaterhead =(ListNode*)malloc(sizeof(ListNode));
        ListNode* pcur = pHead;
         while(pcur)
         {
            if(pcur->val<x)
            {
                //尾插到小链表
                lesstail->next = pcur;
                lesstail = lesstail->next;
            }
            else {
                //尾插到大链表
                greatertail->next = pcur;
                greatertail = greatertail->next;
            }
            pcur = pcur->next;
         }
        //将大链表尾结点置为空,避免死循环,并且这一步一定要在大小链表相连之前
        if(greatertail)
            greatertail->next = NULL;
         //将大小链表相连
         lesstail->next = greaterhead->next;
       
         ListNode* retHead = lesshead->next;
         free(lesshead);
         free(greaterhead); 
         lesshead= greaterhead =NULL;
         return retHead;
    }
};

🌻代码解析

💫 创建带头大小链表,让pcur遍历原链表。如果当前节点值小于val,尾插到小链表,否则就尾插到大链表。注意:最后将大小链表相连前,要将大链表尾结点置为空,防止造成环,与上一题链表尾结点的黏带是一样的问题,最后,保存小链表的有效节点的头结点。将头结点返回,并释放带头大小链表。
该题时间复杂度为O(N),遍历一遍链表,空间复杂度为O(1),只创建有效个变量
🍂画图剖析:
在这里插入图片描述

🐼链表的中间节点

链表的中间节点
🔎题目描述:

💫 给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
在这里插入图片描述
输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3 。

示例 2:
在这里插入图片描述
输入:head = [1,2,3,4,5,6] 输出:[4,5,6] 解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。

📋思路分析

💫用快慢指针,快指针每次走两步,慢指针每次走一步

typedef struct ListNode ListNode;
 struct ListNode* middleNode(struct ListNode* head) {
    ListNode* fast,*slow;

    fast = slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;//慢指针每次走一步
        fast = fast->next->next;//快指针每次走两步
    }
    //slow已经走到中间节点
    return slow;
}

🌻代码解析

定义快慢指针,起始位置都指向head,慢指针每次走一步,快指针每次走两步。当快指针走到空,或者快指针的next指针走到空,此时慢指针指向的节点就是中间节点,直接返回。注意:fast && fast->next顺序不能颠倒,不然可能会造成短路,对空指针的解引用错误。
该题时间复杂度为O(N),遍历一遍链表,空间复杂度为O(1),只创建有效个变量
🍂画图剖析:
在这里插入图片描述

🐼OR36 链表的回文结构

链表的回文结构
🔎题目描述:

💫 对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。

给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

测试样例:
1->2->2->1
返回:true

📋思路分析

🐸思路一:创建新链表,将整个链表反转,用头插的方法,再与原链表一一比较。

class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
         ListNode* pcur = A;
    ListNode* newHead = NULL;

    //头插反转链表
    while(pcur)
    {
       ListNode* next = pcur->next;
       pcur->next = newHead;
       newHead = pcur;
       pcur = next;
    }
    //一一比较
   ListNode* pcur1 = A;
   ListNode* pcur2 = newHead;

    while(pcur1)
    {
        if(pcur1->val!=pcur2->val)
            return false;
        pcur1 = pcur1->next;
        pcur2 = pcur2->next;
    }
   return true;
    }
};

🌻代码解析

创建新链表,采用头插法反转链表,分别取出原链表和新链表的节点值,进行一一比较,只要原链表和新链表的值不相同,就返回false.如果全部遍历完,节点保存的值全都相等,return true.时间复杂度为O(N),遍历两遍链表,空间复杂度为O(1),只创建有效个变量

🐸思路二:题目中要求链表节点数目不会超过900,可以先将链表中的值存放到数组中。再通过判断数组是否为回文结构的方式。

class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
   int arr[900] = {0};
      ListNode* pcur = A;
      int i = 0;
      while(pcur)
      {
       arr[i++] = pcur->val;
        pcur = pcur->next;
      }
      int left = 0;
      int right = i-1;
      while(left<right)
      {
        if(arr[left] != arr[right])
        return false;
        left++;
        right--;
      }
      return true;

    }
};

🌻代码解析

💫用left指向第一个元素的位置,right指向最后一个元素。然后只要left<right,就比较下标为第left和下标为第right的位置,只要值不相同,就返回false.如果全部遍历完,保存的值全都相等,return true.每次更新left和right的值,即left向后走一步,right向前走一步。缺陷(仅限于给定节点个数),不然时间复杂度,空间复杂度会很高,这里属于投机取巧了
时间复杂度为O(N),空间复杂度为O(1),只创建有效个变量

🐸思路三:利用前面算法,找中间节点和反转链表的两个思路,构造两个函数。一个函数用于找中间节点,一个函数用于反转。将链表后半回文结构进行反转,再一一比较链表的前半部分,和后半部分,只要前半部分链表和后半部分链表的值不相同,就返回false.如果全部遍历完,节点保存的值全都相等,return true.(不过小编不推荐,毕竟有点麻烦了,虽然时间复杂度也是O(N),纯粹是做题),。
该题时间复杂度为O(N),遍历三遍链表,空间复杂度为O(1),只创建有效个变量

#include <stdbool.h> 
typedef struct ListNode ListNode;
ListNode* Reverse(ListNode* newhead)
{
    ListNode* n1,*n2,*n3;
    n1 = NULL,n2 = newhead,n3 = newhead->next;
    while(n2)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n3)
            n3 = n3->next;
    }
    return n1;
}

bool isPalindrome(struct ListNode* head)
{
    //快慢指针
    ListNode* fast,*slow;
    fast = slow = head;

    while(fast && fast->next)
    {
        fast = fast->next->next;//快指针走两步
        slow = slow->next;//慢指针走一步
    }
    //新链表起点
    ListNode* newhead = slow;
    //逆序
    newhead = Reverse(newhead);


    ListNode* pcur1 = newhead;
    ListNode* pcur2 = head;

    while(pcur1&&pcur2)
    {
        if(pcur1->val != pcur2->val)
        {
            return false;
        }
        pcur1 = pcur1->next;
        pcur2 = pcur2->next;
    }
    return true;
}

🐼环形链表

环形链表
🔎题目描述:

💫给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。

📋思路分析

🐸还是利用快慢指针。慢指针每次走一步,快指针每次走两步,如果有环,则fast一定会追上slow,即fast = slow;否则,表示链表无环。

typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head)
{
   //空链表,或者只有一个节点
   if(head == NULL)
    return false;

    ListNode* slow,*fast;//快慢指针
    slow = fast = head;
    while(fast && fast->next)
    {
        //如果有环,快指针追上慢指针
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
            return true;
    }
    //走完,没相遇,没环
    return false;
}

🌻代码解析

💫定义快慢指针,起始位置都指向head,慢指针每次走一步,快指针每次走两步。。如果有环,即fast = slow,fast追上slow, return true;否则,链表无环, return false。
该题时间复杂度为O(N),遍历一遍链表,空间复杂度为O(1),只创建有效个变量

🍂画图剖析:
在这里插入图片描述

🐼环形链表||(找到入环节点)

环形链表||
🔎题目描述:

💫给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
在这里插入图片描述
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
在这里插入图片描述
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
在这里插入图片描述
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

📋思路分析

🐸这道题应用结论:环形链表的相遇点到入口节点的距离与链表的起始节点到入口节点的距离一样长。(下面小编会给出证明)

typedef struct ListNode ListNode;
struct ListNode* detectCycle(struct ListNode* head) {
    ListNode *fast, *slow;
    fast = slow = head;
    // 找到相遇点
    while (fast && fast->next) 
    {
        slow = slow->next;
        fast = fast->next->next;
        //有环
        if (fast == slow)
         {
            ListNode* pcur = head;
            // 相遇点到入口的距离与头结点到入口的节点相等
            while (slow != pcur) 
            {
                slow = slow->next;
                pcur = pcur->next;
            }
            // 此时走到入口点
            return pcur;
        }
    }
 return NULL;
}

🌻代码解析

💫首先先找到相遇点,然后利用结论:环形链表的相遇点到入口节点的距离与链表的起始节点到入口节点的距离一样长,用pcur从链表起点和slow从相遇点开始遍历,当slow和pcur相等时,就是入环节点。如果链表无环,则返回空。
该题时间复杂度为O(N),程序总执行次数为N+N次,空间复杂度为O(1),只创建有效个变量
🐬数学证明
在这里插入图片描述

🐼返回倒数第K个节点

返回倒数第K个节点
🔎题目描述:

💫实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。
注意:本题相对原题稍作改动
示例:
输入: 1->2->3->4->5 和 k = 2
输出: 4
说明:
给定的 k 保证是有效的。

📋思路分析

🐸思路:计数器count统计节点个数,倒数第k个节点就是正数第count-k个节点。

typedef struct ListNode ListNode;
int kthToLast(struct ListNode* head, int k){
    int count = 0;//记录总节点个数
    ListNode* pcur = head;
    while(pcur)
    {
        ++count;
        pcur = pcur->next;
    }
    pcur = head;
    int ret = count-k;
    while(ret--)
    {
        pcur = pcur->next;
    }
    return pcur->val;
}

🌻代码解析

💫对原链表的节点个数计数count,pcur遍历原链表,得出节点个数。那么再让pcur从头遍历,走(count-k)次就是倒数第K个节点。
该题时间复杂度为O(N),程序总执行次数为N+N次,空间复杂度为O(1),只创建有效个变量

🐼随机链表的复制

🔎题目描述:

💫给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random该指针可以指向链表中的任何节点或空节点。
💫构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
💫例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
💫用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
示例 1:
在这里插入图片描述
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
在这里插入图片描述
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:
在这里插入图片描述
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

📋思路分析

🐸可以将这个问题分为三步。先拷贝旧链表到新链表,再置random指针,最后再分割链表

typedef struct Node Node;
Node* BuyNode(int x)
{
    Node* node = (Node*)malloc(sizeof(Node));
    if(node == NULL)
    {
        printf("malloc::fail");
        exit(-1);
    }
    node->val = x;
    node->next = node->random = NULL;
    return node;
}
void Copycode(Node* head)
{
    Node*  pcur = head;
    while(pcur)
    {
        Node* next = pcur->next;
        Node* newnode = BuyNode(pcur->val);
        newnode->next = next;
        pcur->next = newnode; 
        pcur = next;
    }
}
void Randomchange(Node* head)
{
     Node*  pcur = head;
    while(pcur)
    {
        if(pcur->random)
            pcur->next->random = pcur->random->next;
            pcur = pcur->next->next;
    }
    
}
   
struct Node* copyRandomList(struct Node* head) {
	//单独处理空链表
    if(head == NULL)
    return head;
    //拷贝旧链表
    Copycode(head);
    //置random指针
    Randomchange(head);
    //分割链表
    Node* pcur = head;
    Node* newhead,*newtail;
    newhead = newtail = pcur->next;
    while(newtail->next)
    {
        pcur = newtail->next;
        newtail->next = pcur->next;
        newtail = newtail->next;
    }
    return newhead;
}

🌻代码解析

💫这道题大家可以结合图形来看(小编在下方给大家每一步做了图).
1,将旧链表拷贝一份新链表,调用**Copycode()**方法,用pcur遍历旧链表,为了找到旧链表的下一个指针,用next保存,接着,调用BuyNode()方法申请新节点newnode(新节点的值与原节点相同),让新节点的后继节点指向pcur的下一个节点(next),pcur的后继节点指向新节点,完成在就链表后插入新节点。
🍂拷贝图像剖析:
在这里插入图片描述
完成在每个旧节点拷贝新节点
2.
置random指针,我们可以轻易从图上找出旧链表random与新链表random之间的关系, pcur->next->random = pcur->random->next
🍂置random指针图像剖析:
在这里插入图片描述
并且解决random指针为空解引用的问题。最后,让pcur每次走两步。走到旧节点的下一个节点。
3,最后将原链表与新链表分割。遍历旧链表,每次保存旧链表的下一个节点,让新链表的尾newtail接到新链表的下一个节点的尾,完成断开操作。
🍂原链表与新链表分割图像剖析:
在这里插入图片描述
在这里插入图片描述
完成分割操作。
💫该题时间复杂度为O(N),程序执行N+N+N次;空间复杂度为O(1),只创建有效个变量

🐼文末

感谢你看到这里,如果觉得本篇文章对你有帮助,点个赞👍 吧,你的点赞就是我更新的最大动力 ⛅️🌈 ☀️

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值