数据结构:单链表OJ

一.引言

上一篇文章我们详细讲解了一下单链表的基础知识,这一篇我们就根据上一篇的内容做一些OJ题进行巩固。

二.OJ实战

1.删除链表元素

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/remove-linked-list-elements/description/数据结构:单链表OJ_第1张图片

方法一:常规方法

根据上一篇的思路,我们通常是引入一个prev,那么就是prev->next=cur->next;free(cur);cur=prev->next;但这里注意不要写成cur=cur->next;因为我们已经free(cur),我们可以再定义cur,但是不能进行访问,这是空指针访问,会产生野指针。

这里还有一个要注意的地方,就是prev->next=cur->next;如果我们第一个元素就要删除,那么prev就是NULL,就会产生空指针访问,会产生野指针。

struct ListNode* removeElements(struct ListNode* head, int val) {
	struct ListNode* prev = NULL;//定义prev,cur
	struct ListNode* cur = head;
	while (cur)
	{
		if (cur->val != val)//没有到达目标元素
		{
			prev = cur;
			cur = cur->next;//prev,cur各往下走一位
		}
		else
		{
			if (prev == NULL)//防止野指针
			{
				head = cur->next;//prev本来就是辅助作用,prev为null用不了,那就只能从自己身上解决了
				free(cur);
				cur = head;
			}
			else
			{
				prev->next = cur->next;
				free(cur);
				cur = prev->next;
			}
		}
	}
	return head;
}
链表快速调试分析模版
int main()
{
	struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));

	n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = NULL;

	removeElements(n1, 7);
}

快速手搓链表,快速调试分析。

方法二:尾指针法

数据结构:单链表OJ_第2张图片

不是val的值,就尾插到新链表。

struct ListNode* removeElements(struct ListNode* head, int val) {
	struct ListNode* newHead = NULL, * tail = NULL;
	struct ListNode* cur = head;
	while (cur)
	{
		if (cur->val != val)
		{
			if (tail == NULL)
			{
				newHead = tail = cur;//空tail没有next
			}
			else
			{
				tail->next = cur;
				tail = tail->next;
			}
			cur=cur->next;//cur要往下走
		}
		else
		{
			struct ListNode* next = cur->next;
			free(cur);
			cur = next;
		}
	}
	if (tail)
	{
		tail->next = NULL;//看注释
	}
	return newHead;
}

if(tail)这条语句,因为链表终止是尾节点->next=NULL,所以要写上tail->next = NULL;同时也是为了防止野指针的产生。

2.链表的中间节点

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/middle-of-the-linked-list/数据结构:单链表OJ_第3张图片

快慢指针法

这道题用快慢指针的方法解决。快慢指针,顾名思义,就是一个指针走得快,一个指针走得慢,因为链表的一个终止条件就是xxxx->next=NULL,所以我们可以用快慢指针解决。

数据结构:单链表OJ_第4张图片

struct ListNode* middleNode(struct ListNode* head){
    struct ListNode* slow,* fast=NULL;
    slow=fast=head;
    while(fast&&fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
    }
    return slow;
}
3.链表中倒数第k个结点

链表中倒数第k个结点_牛客题霸_牛客网输入一个链表,输出该链表中倒数第k个结点。。题目来自【牛客题霸】icon-default.png?t=N7T8https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&&tqId=11167&rp=2&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking数据结构:单链表OJ_第5张图片

这道题也可以用快慢指针,只是用法和上一题不一样,我们快慢指针的核心思想是:通过快指针到达某个条件终止,让慢指针停在目的地。

数据结构:单链表OJ_第6张图片

这样就是fast先走k步,fast和slow拉开k个距离,因为倒数第k个结点到null距离k个,这样slow和fast一起走,当fast到达null时停止,slow也停止,正好到达目的位置。

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    if(pListHead==NULL)//防止引入空链表
    {
        return NULL;
    }
    struct ListNode* slow,* fast=NULL;
    slow=fast=pListHead;
    while(k--)
    {
        if(fast==NULL)//防止k过大导致fast指向空或者next产生野指针
        {
            return NULL;
        }
        fast=fast->next;
    }
    while(fast)
    {
        slow=slow->next;
        fast=fast->next;
    }
    return slow;
}
4.反转链表

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/reverse-linked-list/description/数据结构:单链表OJ_第7张图片

方法一:原链表基础上修改

数据结构:单链表OJ_第8张图片

如图,我们取n1n2n3三个点位,n2目的是取出对应位置,n1是为了给n2衔接,n3是提供下一个点位,所以1和NULL就是一块地方,然后把n2指向n1,与原链表分割开,然后目标节点往下走一个。

struct ListNode* reverseList(struct ListNode* head)
{
    if(head==NULL)
        return NULL;

    struct ListNode*n1,*n2,*n3;
    n1=NULL;
    n2=head;
    n3=n2->next;

    while(n2)
    {
        // 翻转
        n2->next=n1;  //step1
        // 迭代
        n1=n2;        //step2
        n2=n3;        //step3
        if(n3)   //避免最后一步n3为NULL的时候n3->next产生野指针
            n3=n3->next;   //step4
    }
    return n1;
}

步骤:1.先翻转指向;2.把对应节点转入n1;3.引入下一个目标节点;4.更新下一个节点                

方法二:取头结点插入新链表

数据结构:单链表OJ_第9张图片

把1的下一位提取出来,然后把newnode接在1->next,然后整体给newnode,然后取下一位。

struct ListNode* reverseList(struct ListNode* head)
{
	struct ListNode* cur=head,*newhead=NULL;
    while(cur)
    {
        struct ListNode* next=cur->next;
        cur->next=newhead;
        newhead=cur;
        cur=next;
    }
    return newhead;
}
5.合并两个有序链表

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/merge-two-sorted-lists/description/数据结构:单链表OJ_第10张图片

方法一:正常解法

依次比较,较小的尾插。

truct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    if(list1==NULL)
        return list2;
    if(list2==NULL)
        return list1;
    struct ListNode* cur1=list1, *cur2=list2;
    struct ListNode* head=NULL, *tail=NULL;
    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)
        tail->next=cur1;
    if(cur2)
        tail->next=cur2;
    return head;
}

head是记录头结点的位置,以便输出链表,tail是实现尾插。

方法二:哨兵位解法

数据结构:单链表OJ_第11张图片

哨兵位的特点就是我开辟一块空间,然后接上tail,这样的话我们打印出来guard->next,这样的话我们头节点就有了,我们上个方法和该方法思路是一致的,就是记录上头结点,然后往后接,接好后输出头结点,如果没有的话只能输出尾结点。

没有guard的话,我们只能得到链表的尾部,所以要更新tail。

#include 
#include 

typedef int SLTDataType;

typedef struct ListNode
{
    SLTDataType val;
    struct ListNode* next;
}ListNode;

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    if (list1 == NULL)
        return list2;
    if (list2 == NULL)
        return list1;
    struct ListNode* cur1 = list1, * cur2 = list2;
    struct ListNode* guard = NULL, * tail = NULL;
    guard = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
    tail->next = NULL;
    while (cur1 && cur2)
    {
        if (cur1->val < cur2->val)
        {
            tail->next = cur1;
            tail = tail->next;
            cur1 = cur1->next;
        }
        else
        {
            tail->next = cur2;
            tail = tail->next;
            cur2 = cur2->next;
        }
    }
    if (cur1)
        tail->next = cur1;
    if (cur2)
        tail->next = cur2;
    //return guard->next;不这么写是为了防止内存泄漏
    tail=guard->next;
    free(guard);
    return tail;
}

void SLTPrint(ListNode* phead)
{
    ListNode* cur = phead;
    while (cur)
    {
        printf("%d->", cur->val);
        cur = cur->next;
    }
    printf("NULL\n");
}
int main()
{
    struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* n6 = (struct ListNode*)malloc(sizeof(struct ListNode));
    n1->next = n2;
    n2->next = n3;
    n3->next = NULL;
    n4->next = n5;
    n5->next = n6;
    n6->next = NULL;
    
    n1->val = 1;
    n2->val = 2;
    n3->val = 4;
    n4->val = 1;
    n5->val = 3;
    n6->val = 4;

    ListNode* list = mergeTwoLists(n1, n4);
    SLTPrint(list);
}

上面是一个完整的代码,不是OJ体,但是我们要通过此代码彻底理解哨兵位的用法。

首先我们开辟一个guard和一个tail,此时他们指向同一块内存空间。这个时候开始进行比较,但这里就有一个细节,guard->next是什么时候确定的,为什么guard不随tail一起变,他们什么时候内存分割开来。

数据结构:单链表OJ_第12张图片

数据结构:单链表OJ_第13张图片

通过调试窗口,在第一次执行完tail=tail->next的时候,guard和tail的地址就不一样了guard不变,tail换到了next地址上,也就是说,第一次执行后,guard->next就已经被记录了下来,然后tail继续连接数据更新迭代,最后将guard->next交给tail,使其从头部输出,最后free(guard),完成哨兵位解法。所以最关键的就是guard->next(头结点)是什么时候被确定下来的。

//哨兵位,避免了第一个节点为空的情况
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    if(list1==NULL)
        return list2;
    if(list2==NULL)
        return list1;
    struct ListNode* cur1=list1, *cur2=list2;
    struct ListNode* guard=NULL, *tail=NULL;
    guard=tail=(struct ListNode*)malloc(sizeof(struct ListNode));
    tail->next=NULL;
    while(cur1&&cur2)
    {
        if(cur1->val < cur2->val)
        {
            tail->next=cur1;
            tail=tail->next;
            cur1=cur1->next;
        }
        else
        {
            tail->next=cur2;
            tail=tail->next;
            cur2=cur2->next;
        }
    }
    if(cur1)
        tail->next=cur1;
    if(cur2)
        tail->next=cur2;
    tail=guard->next;
    free(guard);
    return tail;
}

哨兵位解法的优势在于不用考虑头结点为空的情况。因为哨兵位的存在,第一个节点必然存在(guard->next),对比与正常解法,正常解法第一个节点(第一个输入对象)一定是空的,所以就要单独考虑,不能直接->next产生野指针。

6.链表分割

链表分割_牛客题霸_牛客网现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的。题目来自【牛客题霸】icon-default.png?t=N7T8https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70?tpId=8&&tqId=11004&rp=2&ru=/activity/oj&qru=/ta/cracking-the-coding-interview/question-ranking

数据结构:单链表OJ_第14张图片

数据结构:单链表OJ_第15张图片

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        ListNode* lGuard,* gGuard,* lTail,* gTail;
        lGuard=lTail=(ListNode*)malloc(sizeof(ListNode));
        gGuard=gTail=(ListNode*)malloc(sizeof(ListNode));
        lTail->next=NULL;
        gTail->next=NULL;
        ListNode* cur=pHead;
        while(cur)
        {
            if(cur->valnext=cur;
                lTail=lTail->next;
            }
            else
            {
                gTail->next=cur;
                gTail=gTail->next;
            }
            cur=cur->next;
        }
        lTail->next=gGuard->next;//小链表和大链表链接
        pHead=lGuard->next;//注意,这里不能用lGuard接收,因为后面会free掉
        gTail->next=NULL;//gTail->next必须置空,一方面要避免空指针访问,另一方面下面细讲。
        free(gGuard);
        free(lGuard);
        return pHead;
    }
};

这里有一个很隐蔽的小bug,就是这个gTail->next=NULL;注意,这行代码必须写,否则会产生bug

数据结构:单链表OJ_第16张图片

为什么会产生这种错误呢?一开始我想到的是野指针问题,但我很快否定了这一观点。我没有对这个空指针进行访问,为什么会报错呢?而且是内存超限问题。

数据结构:单链表OJ_第17张图片

如图,我们只是将pHead的节点复制到了对应的新链表上,也就是说,4和7的下一个点位都还是原先的点位,所以7->next=3,3->next=4,4->next=5,5->next=7,7->next=3,这样的话就形成了一条循环链,就会产生内存超限。所以要gTail->next=NULL。

7.链表的回文结构

链表的回文结构_牛客题霸_牛客网对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为。题目来自【牛客题霸】icon-default.png?t=N7T8https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?tpId=49&&tqId=29370&rp=1&ru=/activity/oj&qru=/ta/2016test/question-ranking数据结构:单链表OJ_第18张图片

数据结构:单链表OJ_第19张图片

总的来说,这一题就是快慢指针和反转链表两题的结合,相对简单。

class PalindromeList {
public:
    struct ListNode* middleNode(struct ListNode* head){
        struct ListNode* slow,* fast=NULL;
        slow=fast=head;
        while(fast&&fast->next)
        {
            slow=slow->next;
            fast=fast->next->next;
        }
        return slow;
    }
    struct ListNode* reverseList(struct ListNode* head)
    {
	    struct ListNode* cur = head, * newhead = NULL;
	    while (cur)
	    {
		    struct ListNode* next = cur->next;
		    cur->next = newhead;
		    newhead = cur;
	    	cur = next;
	    }
	    return newhead;
    }
    bool chkPalindrome(ListNode* head) {
        struct ListNode* mid=middleNode(head);//这里mid是slow,不是头节点
        struct ListNode* rhead=reverseList(mid);//这里是把slow后面的内容逆置
        while(head&&rhead)
        {
            if(head->val!= rhead->val)
            {
                return false;
            }
            else 
            {
                head=head->next;
                rhead=rhead->next;
            }
        }
        return true;
    }
};
8.相交链表

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/intersection-of-two-linked-lists/description/数据结构:单链表OJ_第20张图片

这里先理清一个点,就是交叉链表到底是怎么交叉。注意,一个节点只能有一个next,而多个节点的next可以指向同一个节点。故如下结构就不可能出现:

数据结构:单链表OJ_第21张图片

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode* tailA=headA,*tailB=headB;
    int lenA=1,lenB=1;
    //记录出tailA的长度
    while(tailA->next)
    {
        tailA=tailA->next;
        lenA++;
    }
    //记录出tailB的长度
    while(tailB->next)
    {
        tailB=tailB->next;
        lenB++;
    }
    //如果尾都不交叉,那么就不可能交叉
    if(tailA!=tailB)
        return NULL;
    int gap=abs(lenA-lenB);//两个链表的长度差
    struct ListNode* longlist=headA,*shortlist=headB;
    if(lenAnext;
    }
    while(longlist!=shortlist)//寻找交叉节点
    {
        longlist=longlist->next;
        shortlist=shortlist->next;
    }
    return longlist;
}
9.环形链表

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/linked-list-cycle/description/数据结构:单链表OJ_第22张图片

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;//如果没有环,那么就会出现fast->next=NULL或者fast=NULL,走出循环
}
环形链表引出的相关问题
z1.为什么slow走1步,fast走2步,他们会相遇?会不会错过?请证明。

证明:假设slow进环的时候fast和slow之间的距离为N,slow进环后,fast开始追击slow,slow走一步,fast走两步,他们之间距离缩小1。

fast和slow的距离: N,N-1,N-2......2,1,0       必然能追击上。

z2.slow走1步,fast走n(n>=3),他们还会不会相遇?会不会错过?请证明。

(1).N为偶数:距离N,N-2,N-4......4,2,0

(2).N为奇数:距离N,N-2,N-4......3,1,-1(相当于进入一次新的循环,永远追不上)

10.环形链表(plus)

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/linked-list-cycle-ii/数据结构:单链表OJ_第23张图片

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* slow,*fast;
    fast=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;
}
深入理解环形链表

数据结构:单链表OJ_第24张图片

如图,我们对slow和fast走的距离进行分析。

slow:L+X,      fast:L+n*C+X

因为fast一次两步,slow一次一步,所以fast=2*slow,得到L=n*C-X,故如果一个指针从相遇点走,一个指针从起始点走,则其二者必然相遇,在入口点相遇。

三.总结

上述十道题都是很经典的链表OJ题,读者不妨一试,以加深对链表的了解。

你可能感兴趣的:(数据结构,数据结构,算法)