单链表经典题总结(c语言版)

这些例题具体的解法并不重要,重要的是找到有什么是之前不会的,学习之后会了的。

掌握解决问题的技巧是关键的,再做题中会发现很多没有思路或者有但只有一点思路的题,这个时候打开电脑自带的画图工具画图思考是解决问题关键的一步。

思考所有可能的方法,以免钻牛角尖。

考虑常见的边界情况(对单链表而言是,头尾,空,非空),完善代码。

积累做题的技巧,对于第一次见到的题目可以不会,但是第3次见到类似的题目就一定要做出来。

下面的OJ题是基于单链表的增删查改上的提升

下面做题一定要做三件事:

画图!画图!!画图!!!

单链表经典题总结(c语言版)_第1张图片

 1. 删除链表中等于给定值 val 的所有节点。力扣

单链表经典题总结(c语言版)_第2张图片

注意几种情况:

head为空;一上来就要删除要改变头指针(注意可能会出现空指针的解引用);连续的几个数都要删除;常规情况。

先写出常规的情况再看常规情况是否可以满足上述情况。

struct ListNode* removeElements(struct ListNode* head, int val){

    //如果一上来就要删除
    while(head&&head->val==val)
    {
        struct ListNode* next = head->next;
        free(head);
        head = next;
    }
    struct ListNode*prev = NULL,*cur = head;
    while(cur)
    {
        //如果要连续删除
        if(cur->val==val)
        {
            prev->next = cur->next;
            free(cur);
            cur = prev->next;
        }
        else
        {
            prev = cur;
            cur = cur->next;
        }
        
    }
    return head;
}

如果图也画了,不同情况也考虑了但是走不过就进行调试。有以下几种方法 :

1.走读代码,用大脑走读

2.用图去走读。画图严格跟着写的代码去走。

3.vs编译器上去调试(迫不得已别用)。接口型的代码要自己创建一个情景再去调试。

单链表经典题总结(c语言版)_第3张图片

2. 反转一个单链表。力扣 

单链表经典题总结(c语言版)_第4张图片

第一种方法:新头头插

遍历上面的链表把节点拿下来头插。

struct ListNode* reverseList(struct ListNode* head){
    
    struct ListNode* newhead = NULL;
    while(head)
    {
        struct ListNode* next = head->next;
        head->next = newhead;
        newhead = head;
        head = next;
    }
    return newhead;
}

第二种方法:3指针

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



 下面的代码会崩,原因是再n3=NULL,对NULL进行了解引用

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

所以在迭代的时候还是先把下一个节点用cur/n2的方式保存下来,不要让它自己迭代。 

3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个 中间结点。力扣 

单链表经典题总结(c语言版)_第5张图片

注意本题用快慢指针解题时,要注意fast的结束条件当节点数时偶数时fast==NULL结束,奇数时fast->next==NULL就结束,但在写循环条件的时候还要注意循环条件时fast&&fast->next,不敢写成fast->next&&fast,fast不为空时第二个的前提。如果fast==NULL了就会报一个空指针解引用错误,牛客会报段错误。。

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

类似的单链表指针一次走两步的问题都要考虑奇数偶数 。

4. 输入一个链表,输出该链表中倒数第k个结点。链表中倒数第k个结点_牛客题霸_牛客网 

单链表经典题总结(c语言版)_第6张图片

法一:要求倒数第k个就是求的时整数第n-k个

 法二:快慢指针:fast先走k步,再一起走

注意:

走k步和走k-1步判断结束的条件是不一样的。走k步时fast走到NULL,走k-1步时fast->next==NULL

 用while(k--)这样的循环时,k在跳出循环的时候时-1而不是0,因为在k=0的时候k又减减了一次

while(k--)走了k步,while(--k)走了k-1步

struct ListNode* FindKthToTail(struct ListNode* head, int k ) {
    struct ListNode* slow=head,*fast = head;
    while(fast&&k--)
    {
        fast = fast->next;
    }
    if(k>0)
    {
        return NULL;
    }
    while(fast)
    {
        fast = fast->next;
        slow = slow->next;
    }
    return slow;
}

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

单链表经典题总结(c语言版)_第7张图片

哨兵位是个好东西,有了它上来就可以尾插不用管新头和尾是不是为空

先看一个不加哨兵位的解法: 

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
//传进来的链表有一个是空,就没必要再进行下一步了
    if(list1==NULL)
    return list2;
    if(list2==NULL)
    return list1;
//建立一个尾节点,方便尾插
    struct ListNode*newhead=NULL,*tail = NULL;
//取小的尾插
//只要有一个链表到头了就不用再进行尾插
    while(list1&&list2)
    {
        struct ListNode*newnode = NULL;
        if(list1->valval)
        {
            newnode = list1;
            list1 = list1->next;
        }
        else
        {
            newnode = list2;
            list2 = list2->next;
        }
        if(newhead==NULL)
        {
            newhead = tail = newnode;
        }
        else
        {
            tail->next = newnode;
            tail = newnode;
        }
    }
//把没有到头的链表剩下的部分尾插到新链表的尾
    if(list1)
    {
        tail->next = list1;
    }
    if(list2)
    {
        tail->next = list2;
    }
    return newhead;
}

 看一个有哨兵位的:

 struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    if(list1==NULL)
    return list2;
    if(list2==NULL)
    return list1;
    struct ListNode* newhead,*tail;
    tail = newhead = (struct ListNode*)malloc(sizeof(struct ListNode));
    newhead->next = NULL;
    while(list1&&list2)
    {
        if(list1->valval)
        {
            tail->next = list1;
            tail = list1;
            list1 = list1->next; 
        }
        else
        {
            tail->next = list2;
            tail = list2;
            list2 = list2->next; 
        }
    }
    if(list1)
    tail->next = list1;
    if(list2)
    tail->next = list2;
    struct ListNode*A = newhead->next;
    free(newhead);
    return A;
 }

本质还是链表的增删查改 

当然哨兵位的好处不只是帮助简化代码,还可以避免复杂情况的讨论。 

 6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。链表分割_牛客题霸_牛客网单链表经典题总结(c语言版)_第8张图片

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
//遍历原链表把大于x的尾插到一个链表,小于x的尾插另一个链表,再和前面的题一样,lesstail->next = //greaterhead->next;
        ListNode*lesshead,*lesstail,*greaterhead,*greatertail;
        lesstail = lesshead = (ListNode*)malloc(sizeof(ListNode));
        greatertail = greaterhead = (ListNode*)malloc(sizeof(ListNode));
        while(pHead)
        {
            if(pHead->valnext;
                lesstail->next = pHead;
                lesstail = pHead;
                pHead = next;
            }
            else
            {
                ListNode*next = pHead->next;
                greatertail->next = pHead;
                greatertail = pHead;
                pHead = next;
            }
        }
        greatertail->next = NULL;
        lesstail->next = greaterhead->next;
        ListNode*newhead = lesshead->next;
        free(greaterhead);
        free(lesshead);
        greaterhead = lesshead = NULL;
        return newhead;
            }
};

如果不用哨兵位头节点的话要考虑很多的情况,如:全都是小于x的数,全都是大于x的数,pHead为空。

如果没有把greatertail->next=NULL,牛客会报一个内存超限,大概率是有死循环。在链表中大概率是成环或者迭代的时候忘了cur = cur->next导致了死循环。

ListNode* partition(ListNode* pHead, int x) {
        if(pHead==NULL)
        {
            return NULL;
        }
        ListNode*lesshead,*lesstail,*greaterhead,*greatertail;
        lesstail = lesshead = greaterhead = greatertail = NULL;
        while(pHead)
        {
            ListNode*node = NULL;
            if(pHead->valnext;
                if(lesshead==NULL)
                {
                    lesshead = lesstail = node;
                }
                else
                {
                    lesstail->next = node;
                    lesstail = node;
                }
            }
            else
            {
                node = pHead;
                pHead = pHead->next;
                if(greaterhead==NULL)
                {
                    greaterhead = greatertail = node;
                }
                else
                {
                    greatertail->next = node;
                    greatertail = node;
                }
            }
        }
        //正常情况:
        if(lesshead&&greaterhead)
        {
            greatertail->next=NULL;
            lesstail->next = greaterhead;
            return lesshead;
        }
        //全是大于x的
        else if(lesshead==NULL)
        {
            return greaterhead;
        }
        //全是小于x的
        else if(greaterhead==NULL)
        {
            return lesshead;
        }
        return NULL;

其实也就是这样的差别,哨兵位头节点省脑子。 

 7. 链表的回文结构。链表的回文结构_牛客题霸_牛客网

单链表经典题总结(c语言版)_第9张图片

牛客网上这个题可以遍历一遍用数组把数都存下来,把他当作顺序表中的回文问题。

 但是这是取了巧,下面看看正经的方法:

对于这个题,单链表无法像顺序表或者双向链表一样可以倒着走,所以逆置就成了解决问题的好的方法。清楚了这一点下面的内容也就明了了:

找到中间节点,逆置后面的节点,再一一比较。

  bool chkPalindrome(ListNode* A) {
        // write code here
        //找到中间节点,逆置它后面的,一一比较
        
        ListNode*fast = A,*slow = A;
        while(fast&&fast->next)
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        ListNode*mid = slow,*rhead = NULL;
        while(mid)
        {
            ListNode*next = mid->next;
            mid->next = rhead;
            rhead = mid;
            mid = next;
        }
        while(rhead&&A)
        {
            if(rhead->val==A->val)
            {
                rhead = rhead->next,A = A->next;
            }
            else
            {
                return 0;
            }
        }
        return 1;
    }

但是要注意的是逆置完了后的结构,奇数个节点自然没有问题:

单链表经典题总结(c语言版)_第10张图片

偶数个节点是这样的:

单链表经典题总结(c语言版)_第11张图片

 可以看出来rhead与A是没有完全断开的,叫做链表的相交。但是这个题而言,这样的结构并不影响找回文结构。其他题目中最好还是找到mid的前一个prev,把prev->next=NULL。

这个题也提醒我们,画图真的很重要。

8. 输入两个链表,找出它们的第一个公共结点。力扣 

 单链表经典题总结(c语言版)_第12张图片

 可以暴力一点,,时间复杂度是o(N*M)的算法

下面看另一种:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    //先看看有没又交点,找尾就可以判断,顺便查一下每个链表有几个节点
    struct ListNode *tailA = headA,*tailB = headB;
    int lenA = 1,lenB = 1;//因为在找尾的过程中记录长度所以不会记录最后一个,故而从1开始
    while(tailA->next)
    {
        tailA = tailA->next;
        lenA++;
    }
    while(tailB->next)
    {
        tailB = tailB->next;
        lenB++;
    }
    if(tailA!=tailB)
    {
        return NULL;
    }
    //如果这两个链表和前面说的回文链表一样,通过某种方法使之到公共节点的距离一样就好了
    //这就要用上刚刚记录的len了,让长的链表先走差距步,再一起走,第一个地址相同的节点就是。
    int grap = abs(lenA-lenB);
    //可以写if..else来进行分类讨论,但是有些繁琐所以此处定义两个新的指针,并尝试赋值
    struct ListNode *longList = headA,*shortList = headB;
    if(lenAnext;
    }
    while(longList&&shortList)
    {
        if(longList==shortList)
        return longList;
        longList = longList->next;
        shortList = shortList->next;
    }
    return NULL;
}

最后放一个综合题收尾: 

11. 给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。 要求返回这个链表的深度拷贝。力扣 

 单链表经典题总结(c语言版)_第13张图片

单链表经典题总结(c语言版)_第14张图片

单链表经典题总结(c语言版)_第15张图片

单链表经典题总结(c语言版)_第16张图片

struct Node* copyRandomList(struct Node* head) {
    //复制节点到源节点后面,配置random,拆除复制的节点
	struct Node*cur = head;
    while(cur)
    {
        struct Node*newnode = (struct Node*)malloc(sizeof(struct Node));
        newnode->val = cur->val;
        newnode->next = cur->next;
        cur->next = newnode;
        cur = cur->next->next;
    }
    cur = head;
    while(cur)
    {
        struct Node*copy = cur->next;
       
        if(cur->random)
        copy->random = cur->random->next;
        else
        {
            copy->random = NULL;
        }
        cur = copy->next;
    }
    cur = head;
   struct Node*copyHead = NULL,*copyTail = NULL;
    while(cur)
    {
        struct Node*copy = cur->next;
        cur->next = copy->next;
        if(copyHead==NULL)
        {
            copyHead = copyTail = copy;
        }
        else
        {
            copyTail->next = copy;
            copyTail = copyTail->next;
        }
        cur = cur->next;
    }
    return copyHead;
}

当频繁使用一个相同的变量的时候要初始化如这里的cur

如果每个步骤使用完cur后不再次初始化就会"解答错误"。

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