<数据结构与算法>单链表OJ

本文介绍了10个关于链表的经典编程题目,包括移除链表元素、找到链表中间节点、查找倒数第k个节点、反转链表、合并两个有序链表等。通过这些题目,读者可以巩固链表操作,如快慢指针、头插法、尾插法等技巧。文章强调了在解决链表问题时,掌握基础操作和灵活运用解题策略的重要性。
摘要由CSDN通过智能技术生成

目录

前言

 一、OJ练习 

 1.移除链表元素

 2.链表的中间结点

 3.链表中倒数第k个结点

 4.反转链表

 5. 合并两个有序链表

 6.链表分割

 7. 链表的回文结构

 8.相交链表

 9.环形链表

10. 环形链表 II

总结


前言

学习完单链表的增删查改,我们就需要练习来巩固,单链表中的题目无非就是增删查改的排列组合,根据实际情况采用较为方便的方式解题,这里小帅带大家练习链表经典题目。


 一、OJ练习 

1.移除链表元素

 方法一:双指针

使用两个指针,一个在前一个在后,前指针找到目标值开始删除操作,但是这个方法有许多坑

  1. 在删除完节点并释放后,不能再使用该空间
  2. 如果头节点就是要删除的节点,不进行判断就会出现错误

 

struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode *cur=head,*prev=NULL;
    while(cur)//遍历
    {
        if(cur->val != val)
        {
            prev = cur;
            cur = cur->next;
        }
        else
        {
            if(cur == head)
            {
                head = cur->next;
                free(cur);
                cur = head;//从头开始
            }
            else
            {
                prev->next = cur->next;
                free(cur);
                cur = prev->next;//不能再使用cur赋值,因为它已经被释放了
            }
        }
    }
    return head;
}

方法二:创建新链表,比较后赋值(双指针) 

struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* newList = NULL, *tail = NULL, *cur = head;//tail记录新链表的尾节点,如果是记录头节点,每次都要找尾节点,时间繁琐
    while(cur)//遍历
    {
        if(cur->val != val)
        {
            if(tail == NULL)//这时新链表为空
            {
                newList = cur;//将头指针更新
                tail = newList;//赋值tail
            }
            else//新链表不为空
            {
                tail->next = cur;
                tail = tail->next;
            }
            cur = cur->next;//cur后一一个节点
        }
        else
        {
            struct ListNode* next = cur->next;//保存下一个节点,为cur释放后赋值
            free(cur);
            cur = next;
        }
    }
    if(tail)//如果是空指针就不能进行下面的赋值操作
        tail->next = NULL;
    return newList;
}

 力扣想调试,就自己手写一个简单的main函数,快速手搓一个链表

 2.链表的中间结点

方法:快慢指针

慢指针一次走一个节点,快指针一次走两个节点 。

情况分奇偶节点数:

  • 奇数个,fast->next == NULL
  • 偶数个,fast == NULL

 

struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode *fast = head, *slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast ->next ->next;
    }
    return slow;
}

 3.链表中倒数第k个结点

 方法:快慢指针

快指针走法有两种,由于倒数第k个到最后1个之间的距离是k-1

  1. 走k-1次,之后fast与slow每次都走1,直到fast->next = NULL
  2. 走k次,之后fast与slow每次都走1,直到 fast 为NULL

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k)
{
    struct ListNode* fast, * slow;
    fast = slow = pListHead;
    while (k--)//循环k次,如果是--k是k-1次
    {
        if (fast == NULL)//判断k是否大于链表长度
        {
            return NULL;
        }
        fast = fast->next;
    }
    while (fast)//直到fast为空,两指针同时走
    {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

4.反转链表

方法一:三指针反转链表 

  • n1,n2负责反转,n3负责移动找到下一节点

 

//方法一:三指针法,反转链表
 struct ListNode* reverseList(struct ListNode* head)
 {
     if (head == NULL)//head不能为空
     {
         return NULL;
     }
     struct ListNode* n1, * n2, * n3;
     n1 = NULL;
     n2 = head;//n1, n2负责反转
     n3 = n2->next;//n3负责找到下一个节点
     while (n2)
     {
         //反转
         n2->next = n1;
         //移动
         n1 = n2;
         n2 = n3;
         //n3为NULL时就不能进行赋值操作,所以要判断一下
         if(n3)
            n3 = n3->next;
     }
     return n1;
 }

方法二:创建新的头节点,头插 

 


 struct ListNode* reverseList(struct ListNode* head)
 {
     struct ListNode* cur = head, * newlist = NULL;
     while (cur)
     {
         struct ListNode* next = cur->next;
         //头插
         cur->next = head;
         head = cur;
         //移动
         cur = next;
     }
     return newlist;
 }

5. 合并两个有序链表

方法一: 比大小,小的尾插至新链表

但是要注意链表为空的情况

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    if (list1 == NULL)//判断链表是否为空,如果不处理的话,后面程序会出现NULL错误
        return list2;
    if (list2 == NULL)
        return list1;

    struct ListNode* cur1 = list1, * cur2 = list2;//两个指针负责移动
    struct ListNode* head = NULL, * tail = NULL;//tail负责找尾

    while (cur1 && cur2)
    {
        if (cur1->val < cur2->val)
        {
            if (head == NULL)
            {
                head = tail = cur1;//链表为空,进行赋值
            }
            else
            {
                tail->next = cur1;//尾插
                tail = tail->next;
            }
            cur1 = cur1->next;
        }
        else
        {
            if (head == NULL)//同理
            {
                head = tail = cur2;
            }
            else
            {
                tail->next = cur2;
                tail = tail->next;
            }
            cur2 = cur2->next;
        }
    }
    if (cur1)//如果链表一没有放完,直接把cur1之后都链上
        tail->next = cur1;
    if (cur2)//如果链表二没有放完,直接把cur1之后都链上
        tail->next = cur2;

    return head;
    
}

方法二:同方法一,但是加上了哨兵位 

  • 哨兵位可以不用再检查链表是否为空,因为哨兵位不是空指针,它避免了使用NULL指针进行赋值操作(尾插比较需要哨兵位,头插基本不需要)

 

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    struct ListNode* cur1 = list1, * cur2 = list2;
    struct ListNode* guard = NULL, * tail = NULL;
    guard = tail = (struct ListNode*)malloc(sizeof(struct ListNode));//哨兵位必须单独开辟节点
    tail->next = NULL;//控制哨兵位的next为空

    while (cur1 && cur2)
    {
        if (cur1->val < cur2->val)
        {
            tail->next = cur1;//因为tail不为空,所以不需要判断为空的情况
            tail = tail->next;
            cur1 = cur1->next;
        }
        else
        {
            tail->next = cur2;
            tail = tail->next;
            cur2 = cur2->next;
        }
    }
    if (cur1)//如果链表一没有放完,直接把cur1之后都链上
    {
        tail->next = cur1;
    }
    if (cur2)//如果链表二没有放完,直接把出cur2之后都链上
    {
        tail->next = cur2;
    }

    struct ListNode* head = guard->next;//注意!如果不释放单独开辟的guard指针,会造成内存泄露
    free(guard);
    guard = NULL;
    //返回的是头节点,不是哨兵位,所以返回哨兵位的next
    return head;
}

 6.链表分割

方法一:创建两个链表,分别存放小于x,大于等于x,最后再链接

  • 这里最好带哨兵位,如果不用,那么我们需要去判断许多为空的情况,都大于x,或都小于x
  • 尾插没有改变next,所以不需要提前存放当前指针的next是这样很容易会忘记将尾指针的next置空,导致链表循环在头插中,由于next改变所以需要提前存储当前指针的next 

NULL情况太多,建议使用哨兵位

 

单链表题目中,无非是增删查改的操作的组合,掌握基本增删查改函数是基本功。保持简单的头插或者尾插是一个简单的解题思路如果要中间插入或删除,试着想一想能头插尾插吗

要在一个链表内又删除又插入,是一个情况繁多新手很难把握的程序,例如此题,我们可能会有把链表中大于x的进行尾插的想法,但是稍微一分析,就会发现有很多情况要分,删除操作要找上一个节点和下一个节点,上一个节点是否为头节点?尾插一直循环,到何时停止?

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) 
    {
        struct ListNode* gGuard, * gTail, * lGuard, * lTail;//两个哨兵位,以及两个尾指针
        gGuard = gTail = (struct ListNode*)malloc(sizeof(struct ListNode));//为哨兵位赋值结构体指针
        lGuard = lTail = (struct ListNode*)malloc(sizeof(struct ListNode));
        gTail->next = lTail->next = NULL;//置空

        struct ListNode* cur = pHead;
        while (cur)//cur遍历
        {
            if (cur->val < x)
            {
                lTail->next = cur;
                lTail = lTail->next;//指向尾节点
            }
            else
            {
                gTail->next = cur;
                gTail = gTail->next;//指向尾节点
            }
            cur = cur->next;//cur移动
        }
        lTail->next = gGuard->next;//链接
        gTail->next = NULL;//尾节点next置空,不置空会出现链表循环

        pHead = lGuard->next;//存放链表头节点return
        free(gGuard);//释放
        free(lGuard);

        return pHead;
    }
};

7. 链表的回文结构

方法:先找到中间节点 ,再将中间节点之后的链表逆置,再比较是否相等即可

(如果是逆置整个链表是不行的,因为逆置改变了原链表,只能复制一个原链表在进行逆置)

struct ListNode
{
	int val;
	struct ListNode* next;
};

//寻找中间节点函数
struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* fast = head, * slow = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

//链表逆置函数
struct ListNode* reverseList(struct ListNode* head) {
    if (head == NULL) { //head不能为空
        return NULL;
    }
    struct ListNode* n1, * n2, * n3;
    n1 = NULL;
    n2 = head;//n1, n2负责反转
    n3 = n2->next;//n3负责找到下一个节点
    while (n2) {
        //反转
        n2->next = n1;
        //移动
        n1 = n2;
        n2 = n3;
        //n3为NULL时就不能进行赋值操作,所以要判断一下
        if (n3)
            n3 = n3->next;
    }
    return n1;
}

bool chkPalindrome(ListNode* head)
{
    struct ListNode* mid = middleNode(head);
    struct ListNode* rhead = reverseList(mid);

    while (head && rhead)//有一个链表为空就停止比较
    {
        if (head->val != rhead->val)
            return false;

        head = head->next;
        rhead = rhead->next;
    }
    return true;
}

8.相交链表

方法:

  • 分别求两个链表的长度
  • 长的链表先走差距步
  • 同时走,第一个地址相同的就是交点

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
   struct ListNode*  tailA = headA, *tailB = headB;//两个指针负责找到两个链表的最后节点
   int lenA = 1, lenB = 1;//记录两个链表的长度
   while(tailA->next)//找到最后一个节点,并记录长度
   {
       tailA = tailA->next;
       ++lenA;
   }
   while(tailB->next)
   {
       tailB = tailB->next;
       ++lenB;
   }

   if(tailA != tailB)//判断是否相交
   return NULL;

   int gap =fabs(lenA-lenB);//算相差长度
   struct ListNode* longList = headA, *shortList = headB;//从头比较
   if(lenA<lenB)//如果上面长短错了,再换一下
   {
       longList = headB;
       shortList = headA;
   }

   while(gap--)//长链表指针移动相差步长
   {
       longList = longList->next;
   }

   while(longList != shortList)//比较两链表节点地址是否相同
   {
       longList = longList->next;
       shortList = shortList->next;
   }
   return longList;//返回相遇的节点地址
}

9.环形链表

 方法:快慢指针,追击问题

struct ListNode 
{
	int val;
	struct ListNode* next;
	
};

bool hasCycle(struct ListNode* head)
{
	struct ListNode* fast = head, * slow = head;
	while (fast && fast->next)//因为fast一次走两步,所以要两种判断
	{
		slow = slow->next;
		fast = fast->next->next;

		if (slow == fast)//相遇,就返回真
			return true;
	}
	return false;//循环退出,那么就表明没有循环
}

 拓展问题:

  1. 为什么slow1步,fast走2步,它们会相遇?会不会错过?请证明
  2. 如果slow1步,fast走m步(m>=3),它们是否会相遇?是否会错过?请证明

 

 

 

10. 环形链表 II

分析: 

 

结论:一个指针从相遇点走,一个指针从起始点走,会在入口点相遇 

//方法一:理论推导  L = n*C - X
struct ListNode* detectCycle(struct ListNode* head) 
{
	struct ListNode* fast = head, * slow = head;
	while (fast && fast->next)
	{
		slow = slow->next;
		fast = fast->next->next;

		if (slow == fast)
		{
			struct ListNode* meet = slow;
			struct ListNode* start = head;

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

 方法二:将相遇点与相遇点的下一节点之间断开,转换成相交链表找交点的问题

// 方法二:将相遇点与相遇点的下一节点之间断开,转换成相交链表找交点的问题
struct ListNode* detectCycle(struct ListNode* head)
{
	struct ListNode* fast = head, * slow = head;
	int len1 = 1, len2 = 1;//置为1是因为要找到两个链表的最后一个节点,循环条件是tail->next != NULL,少算一个所以初值为1
	while (fast && fast->next)
	{
		slow = slow->next;
		fast = fast->next->next;

		if (slow == fast)
		{
			//断开节点
			struct ListNode* start = slow->next;
			slow->next = NULL;
			//寻找相交节点
			struct ListNode* tail1 = head, * tail2 = start;
			while(tail1->next)
			{
				len1++;
				tail1 = tail1->next;
			}
			while (tail2->next)
			{
				len2++;
				tail2 = tail2->next;
			}
			int gap = fabs(len1 - len2);
			struct ListNode* longlist = head, *shortlist = start;
			if (len1 < len2)
			{
				longlist = start;
				shortlist = head;
			}
			while (gap--)
			{
				longlist = longlist->next;
			}
			while (longlist != shortlist)
			{
				longlist = longlist->next;
				shortlist = shortlist->next;
			}
			return longlist;
		}
	}
	return NULL;
	
}

11. 复制带随机指针的链表

分析:本题关键就在于如何复制各节点内random指针指向的节点的信息 ,因为题目要求新链表各节点不能指向原链表,所以直接赋值给新节点是错误的,那么我们思考如何储存当前指针cur与其random指针指向的节点之间的相对位置,遍历数组找到与cur->random->val值相同的节点?不可以,因为如果有大于等于2个节点的val相同,那么random就可能会找错,造成错误。既然找相同值不可以,那我们找与cur->random地址相同的节点,并使用计数器记录找到该节点共经过了几个节点,对每个节点的random都遍历一次链表,或者用指针数组存各节点random的值,再建立一个数组,遍历链表与指针数组值比较,将计数器记录下来的值存放至数组,这两种方法都可行,但很显然时间复杂度很大。

这里有很优秀的解法

  1. 将原链表各节点后插入一个与其val值相同的节点
  2. 将原链表原节点赋值copy->random = cur->random->next
  3. 最后取下各复制的节点尾插到新链表并恢复原链表

如果链表掌握不是很优秀的话,即使知道了解题思路也很难直接通过,可能总是会因指针问题调试,所以独立完成此题就是检验链表是否完美掌握的标志 

struct Node 
{
	int val;
	struct Node* next;
	struct Node* random;
}; 

struct Node* copyRandomList(struct Node* head) 
{
	//1.创建新节点,插入链表
	struct Node* cur = head;
	while (cur)
	{
		struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
		struct Node* next = cur->next;
		copy->val = cur->val;

		copy->next = next;
		cur->next = copy;

		cur = next;
	}
	//2.将当前节点random->next赋值给下一个节点的random
	cur = head;
	while (cur)
	{
		struct Node* copy = cur->next;

		if (cur->random == NULL)
		{
			copy->random = NULL;
		}
		else
		{
			copy->random = cur->random->next;
		}

		cur = cur->next->next;
	}
	//3.解下来,尾插
	struct Node* newlist = NULL, *tail = NULL;
	cur = head;
	while (cur)
	{
		struct Node* copy = cur->next;//cur不可能为空,那么copy会为空,那么tail->next就会为空,
		//所以最后不需要再加tail->next为空,反而会把程序搞错
		struct Node* next = copy->next;
		
		if (newlist == NULL)
		{
			newlist = tail = copy;
		}
		else
		{
			tail->next = copy;
			tail = tail->next;
		}
		cur->next = next;
		cur = next;
	}
	
	return newlist;
}

 

总结

        至此我们练习了较为经典的单链表题目,更加深刻的理解结构体指针与单链表增删查改操作,跨过诸多链表小坑,想要在做题中有较为优秀的思路,只能多做多练,见得多自然也就有优秀的解题思路了。

         最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值