【数据结构】链表经典算法题集锦

前言:本章将分享十一道来自LeetCode/牛客网中的经典链表算法题来介绍数据结构中链表在算法题中的应用。


文章目录

      • 1.删除链表元素
        • 思路分析:
        • 题解:
      • 2.反转链表
        • 思路分析:
        • 题解:
      • 3.链表中间结点
        • 思路分析(快慢指针法):
        • 题解:
      • 4.链表中倒数第K个结点
        • 思路分析(快慢指针法):
        • 题解:
      • 5.合并两个有序链表
        • 思路分析:
        • 题解:
      • 6.链表分割
        • 思路分析:
        • 题解:
      • 7.链表的回文结构(第2题和第3题的综合)
        • 思路分析:
        • 题解:
      • 8.相交链表
        • 思路分析:
        • 题解:
      • 9.环形链表I
        • 思路分析:
        • 题解:
      • 10.环形链表II
        • 思路分析:
        • 题解:
      • 11.复制带随机指针的链表(这题卡了好几次,多做做)
        • 思路分析:
        • 题解:


1.删除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点。OJ链接

【数据结构】链表经典算法题集锦_第1张图片


思路分析:

1.遍历找到需要删除的结点前一个结点,即如果cur->next->val=val,说明cur就是需要删除结点的前一个结点

2.让需要删除结点的前一个结点与需要删除结点的后一个结点连接,即cur->next = cur->next->next

3.释放需要删除的结点

遍历方式:

如果 cur的下一个节点的节点值不等于给定的 val,则保留下一个节点,将 val移动到下一个节点即可。当 cur的下一个节点为空时,链表遍历结束。
此过程与单链表的头删操作类似,但是单链表的头删中对一个结点和多个结点是进行分情况讨论的,这就会比较麻烦,我们可以给原来链表增加一个哑结点,避免分类讨论。

题解:

struct ListNode* removeElements(struct ListNode* head, int val){
     
    struct ListNode* node=(struct ListNode*)malloc(sizeof(struct ListNode));
    node->next=head; //创建哑结点,链接
    struct ListNode* cur=node;//迭代 

    while(cur->next)
    {
     
        if(cur->next->val==val)
        {
     
            struct ListNode*x=cur->next;
            cur->next=cur->next->next;
            free(x);           //释放被删除结点空间
        }
        else
        {
     
            cur=cur->next;
        }
    }
    return node->next;
}

2.反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。OJ链接

【数据结构】链表经典算法题集锦_第2张图片


思路分析:

假设链表为 1→2→3→∅,我们想要把它改成∅←1←2←3。

在遍历链表时,将当前节点的 \textit{next}next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。

具体步骤:

1.新建一个prev指针置为NULL,cur指向head,新建一个next。

2.进入while循环,next保存cur下一个结点数据,同时cur指向前一个结点prev,然后prev移动到当前cur位置,cur移动到当前next位置,以此循环

题解:

struct ListNode* reverseList(struct ListNode* head) 
{
     
    struct ListNode* prev = NULL;//新建一个prev指针置为NULL
    struct ListNode* cur = head;//cur指针赋值为头结点head
    struct ListNode* next= NULL;//新建一个next
    while (cur) 
    {
     
        next = cur->next;//next保存下一个结点
        cur->next = prev;//cur指向前一个结点prev
        prev = cur;     //prev移动到当前cur位置
        cur = next;    //cur移动到当前next位置
    }
    return prev;
}

3.链表中间结点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。OJ链接

【数据结构】链表经典算法题集锦_第3张图片


思路分析(快慢指针法):

即快指针fast比慢指针slow多走一步,这样当快指针走完整个链表时候,慢指针就刚好是中间结点.其中快指针的结束条件受到结点个数的奇偶影响.

1.当结点数是偶数时候,当fast为空,即fast=NULL,slow满足题解

2.当结点数是奇数时候,当fast为尾结点,即fast->next=NULL,slow满足题解

题解:

struct ListNode* middleNode(struct ListNode* head)
{
     
    struct ListNode* fast,*slow; //定义快慢指针
    fast = slow = head;
    while(fast && (fast->next)) 
    // 当fast为空,或者fast->next为空,就结束循环,说明此时slow已经走到了中间结点
    {
     
        fast = fast->next->next;//fast走两步
        slow = slow->next; //slow走一步
    }
    return slow;
}

4.链表中倒数第K个结点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。OJ链接

【数据结构】链表经典算法题集锦_第4张图片


思路分析(快慢指针法):

先让fast走K步,然后slow与fast以相同的速度,当fast走到末尾时跳出(fast->next=NULL),跳出后, slow 与尾节点距离为 k-1,即 slow 指向倒数第 k 个节点)

【数据结构】链表经典算法题集锦_第5张图片

题解:

struct ListNode* getKthFromEnd(struct ListNode* head, int k){
     
    struct ListNode* slow,*fast;
    slow = fast = head;   //创建快慢指针
    
    while(k--)   //fast先走K步
    {
     
        if(fast)        //注意!!!!这里必须要判断,为什么呢?因为题目并没有规定k是小于等于链表长度的哦
            fast = fast->next;
        else
            return NULL;
    }
    while(fast)  //同步
    {
     
        fast = fast->next;
        slow = slow->next;
    }
    return slow;
}

5.合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 OJ链接

【数据结构】链表经典算法题集锦_第6张图片


思路分析:

1.新建立一个新数组,建立一个哑结点

2.然后数组上下两两比较,小的就依次放在新数组的哑结点后。

题解:

struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
     
    struct ListNode* p1 = l1,*p2 = l2;
    struct ListNode* newhead = NULL,*tail = NULL;

    if(p1==NULL)
    {
     
        return p2;
    }
    if(p2==NULL)
    {
     
        return p1;
    }
    
    newhead = tail = (struct ListNode* )malloc(sizeof(struct ListNode));
    //创建新结点

    while(p1 && p2)  //开始一一比较并进行连接
    {
     
        (p1-> val) <= (p2->val) ?  //比较谁小
        (tail->next = p1,p1 = p1->next,tail = tail->next) :  
        //p1的值更小,则连接p1,然后tail和p1迭代
        (tail->next = p2,p2 = p2->next,tail = tail->next);  
        //p2的值更小,则连接p2,然后tail和p2迭代
    }
    //特别注意下这里,第一次刷犯错了
    p1 ? (tail->next = p1):(p1);  //如果p1还有值,则把剩下的p1连接
    p2 ? (tail->next = p2):(p2);  //如果p2还有值,则把剩下的p2连接

    struct ListNode* pp = newhead->next;  //保存哑结点后面的结点
    free(newhead); //释放哑结点
    newhead = NULL;
    return pp;
}

这里记录下刷这道题时烦得错误

【数据结构】链表经典算法题集锦_第7张图片

【数据结构】链表经典算法题集锦_第8张图片

真是题目做得脑子坏了,就算是代替条件判断语句也是if来替代


6.链表分割

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你不需要 保留 每个分区中各节点的初始相对位置。OJ链接

【数据结构】链表经典算法题集锦_第9张图片


思路分析:

同时创建两个哑结点,值小于x的结点放在第一个结点后,值大于等于x的结点放在第二个结点后,等原链表放完后,再把第二个哑结点链表后面的数据连接在第一个哑结点链表上

题解:

struct ListNode* partition(struct ListNode* head, int x) {
     
    struct ListNode* small = malloc(sizeof(struct ListNode));
    struct ListNode* smallHead = small;
    struct ListNode* large = malloc(sizeof(struct ListNode));
    struct ListNode* largeHead = large;
    while (head) {
     
        if (head->val < x) {
     
            small->next = head;
            small = small->next;
        } else {
     
            large->next = head;
            large = large->next;
        }
        head = head->next;
    }
    large->next = NULL;
    small->next = largeHead->next;//把large哑结点后的结点链接给small
    return smallHead->next;//返回small哑结点后的结点
}

7.链表的回文结构(第2题和第3题的综合)

OJ链接

对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

测试样例:

1->2->2->1
返回:true

思路分析:

可以先找到中间节点,然后把后半部分逆置,最近前后两部分一一比对,如果节点的值全部相同,则即为回文。

题解:

class PalindromeList {
     
public:
	bool chkPalindrome(ListNode* A) {
     
		if (A == NULL || A->next == NULL)
			return true;
		ListNode* slow, *fast, *prev, *cur, *nxt;
		slow = fast = A;
		//找到中间节点
		while (fast && fast->next)
		{
     
			slow = slow->next;
			fast = fast->next->next;
		}
		prev = NULL;
		//后半部分逆置
		cur = slow;
		while (cur)
		{
     
			nxt = cur->next;
			cur->next = prev;
			prev = cur;
			cur = nxt;
		}
		//逐点比对
		while (A && prev)
		{
     
			if (A->val != prev->val)
				return false;
			A = A->next;
			prev = prev->next;
		}
		return true;
	}
};

**怎么样,是不是感觉就是寻找链表中间数和反转链表两道题的综合。**题目难度为较难级别,但是有了前文寻找中间元素和链表元素逆置的基础,这题反而更像入门级别的送分题。


8.相交链表

OJ链接

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:

【数据结构】链表经典算法题集锦_第10张图片

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

【数据结构】链表经典算法题集锦_第11张图片

【数据结构】链表经典算法题集锦_第12张图片

思路分析:

此题可以先计算出两个链表的长度,让长的链表先走相差的长度,然后两个链表同时走,直到遇到相同的节点,即为第一个公共节点

题解:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
     
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
     
        int lenA=0,lenB=0;
        ListNode* curA=headA,*curB=headB;
        //计算两个链表的长度
        while(curA)
        {
     
            lenA++;
            curA=curA->next;
        }
        while(curB)
        {
     
            lenB++;
            curB=curB->next;
        }
        //
        int len=abs(lenA-lenB);
        ListNode* LongList=headA,*ShortList=headB;
        if(lenA<lenB)
        {
     
            LongList=headB;
            ShortList=headA;
        }
        //让长链表先走len步
        while(len--)
        {
     
            LongList=LongList->next;
        }
        //双链表同时走,直到相遇
        while(LongList &&ShortList)
        {
     
            if(LongList==ShortList)
            return LongList;
            else
            {
     
                LongList=LongList->next;
                ShortList=ShortList->next;
            }
        }
        return NULL;
    }
};

9.环形链表I

OJ链接

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。

进阶:

你能用 O(1)(即,常量)内存解决此问题吗?

【数据结构】链表经典算法题集锦_第13张图片

思路分析:

定义快慢指针fast,slow, 如果链表确实有环,fast指针一定会在环内追上slow指针。

题解:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
bool hasCycle(struct ListNode *head) {
     
    struct ListNode*slow,*fast;
    slow=fast=head;

    while(fast && fast->next)
    {
     
        slow=slow->next;
        fast=fast->next->next;

        if(slow==fast)
        return true;
    }
    return false;
}

10.环形链表II

OJ链接

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。

进阶:

你是否可以使用 O(1) 空间解决此题?

【数据结构】链表经典算法题集锦_第14张图片

思路分析:

如果链表存在环,则fast和slow会在环内相遇,定义相遇点到入口点的距离为X,定义环的长度为C,定义头到入口的距离为L,fast在slow进入环之后一圈内追上slow,则会得知:

slow所走的步数为:L + X

fast所走的步数为:L + X + N * C

并且fast所走的步数为slow的两倍,故:2*(L + X) = L + X + N * C

即: L = N * C - X

所以从相遇点开始slow继续走,让一个指针从头开始走,相遇点即为入口节点

比较难理解,这里画下图

1.套用环形链表I的思路,快慢指针找到相遇点

【数据结构】链表经典算法题集锦_第15张图片

2.根据我们思路分析得出的结论:从相遇点开始slow继续走,让一个指针从头开始走,相遇点即为入口节点,以此来寻找入口点。

题解:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode Node;
struct ListNode *detectCycle(struct ListNode *head) {
     
    Node* slow,*fast;
    slow=fast=head;

    while(fast && fast->next)
    {
     
        slow=slow->next;
        fast=fast->next->next;

        //标记相遇点,并重新建个指针指向头
        if(slow==fast)
        {
     
            Node* meet=slow;
            Node* start=head;

            while(meet!=start)
            {
     
                meet=meet->next;
                start=start->next;
            }
        return meet;
        }
    }
    return NULL;
}

11.复制带随机指针的链表(这题卡了好几次,多做做)

OJ链接

给你一个长度为 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 作为传入参数。

【数据结构】链表经典算法题集锦_第16张图片

思路分析:

1.拷贝链表的每一个节点,拷贝的节点先链接到被拷贝节点的后面

2.复制随机指针的链接:拷贝节点的随机指针指向被拷贝节点随机指针的下一个位置

3.拆解链表,把拷贝的链表从原链表中拆解出来

题解:

class Solution {
     
public:
    Node* copyRandomList(Node* head) {
     
        // 1.拷贝链表,并插入到原节点的后面
        Node* cur = head;
        while(cur)
        {
     
            Node* next = cur->next;
 
            Node* copy = (Node*)malloc(sizeof(Node));
            copy->val = cur->val;
 
            // 插入
            cur->next = copy;
            copy->next = next;
 
            // 迭代往下走
            cur = next;
        }
 
        // 2.置拷贝节点的random
        cur = head;
        while(cur)
        {
     
            Node* copy = cur->next;
            if(cur->random != NULL)
                copy->random = cur->random->next;
            else
                copy->random = NULL;
 
            cur = copy->next;
        }
 
        // 3.解拷贝节点,链接拷贝节点
        Node* copyHead = NULL, *copyTail = NULL;
        cur = head;
        while(cur)
        {
     
            Node* copy = cur->next;
            Node* next = copy->next;
 
            // copy解下来尾插
            if(copyTail == NULL)
            {
     
                copyHead = copyTail = copy;
            }
            else
            {
        
                copyTail->next = copy;
                copyTail = copy;
            }
 
            cur->next = next;
 
            cur = next;
        }
 
        return copyHead;
    }
};

数据结构的链表LeetCode练习内容到此介绍结束了,感谢您的阅读!!!如果内容对你有帮助的话,记得给我三连(点赞、收藏、关注)——做个手有余香的人。

你可能感兴趣的:(数据结构初阶,链表,算法,leetcode)