Bootstrap

【数据结构】链表经典练习题

-----------------------------------单链表习题------------------------------------

1.移除单链表元素

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

题目链接:力扣203. 移除链表元素.

题解:迭代法

  • 时间复杂度O(N)、空间复杂度O(1)
  • 由于有可能头结点就是需要删除的结点,所以给链表加一个空头结点
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;

struct ListNode* removeElements(struct ListNode* head, int val)
{
    //迭代法
    //由于有可能头结点就是需要删除的结点,所以给链表加一个空头结点
    ListNode* EmptyNode = (ListNode*)malloc(sizeof(ListNode));
    EmptyNode->next = head;

    //加一个遍历指针
    ListNode* cur = EmptyNode;
    while(cur->next)
    {
        if(val == cur->next->val)
        {
            cur->next = cur->next->next;
        }
        else
        {
            cur = cur->next;
        }
    }
    return EmptyNode->next;
}

2.逆转单链表

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

题目链接:力扣206. 反转链表.

题解:迭代法

  • 时间复杂度O(N)、空间复杂度O(1)
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

typedef struct ListNode ListNode;

ListNode* reverseList(struct ListNode* head)
{
    ListNode* cur = head;
    ListNode* prev = NULL;
    while(cur)
    {
        ListNode* next = cur->next; //先保留cur结点的next指向
        cur->next = prev;  //反转cur的next指向
        prev = cur; //更新prev
        cur = next;  //更新cur
    }
    return prev;
}

3.获取链表的中间结点

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

题目链接:力扣876. 链表的中间结点.

题解1:迭代法【own】

  • 时间复杂度O(N)、空间复杂度O(1)
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

//循环迭代法
int ListNodeSize(struct ListNode* head)  //求链表长度
{
    struct ListNode* cur = head;
    int count = 0;
    while(cur)
    {
        count++;
        cur = cur->next;
    }
    return count;
}

struct ListNode* middleNode(struct ListNode* head)
{
    int size = ListNodeSize(head)/2;
    struct ListNode* cur = head;
    while(size--)
    {
        cur = cur->next;
    }
    return cur;
}

题解2:快慢指针法

设置两个快慢指针一起从链表头结点遍历,快指针每次循环走两步,满指针每次一步,当快指针走到链表尾部时,慢指针指向的结点即为中点。

  • 时间复杂度O(N/2)、空间复杂度O(1)
  • 注意:while(fast && fast->next)中的判定条件先后顺序不能变化 ,若颠倒后有可能会出现指针越界。
//快慢指针法
//设置两个快慢指针一起从链表头结点遍历
//快指针每次循环走两步,满指针每次一步
//当快指针走到链表尾部时,慢指针指向的结点即为中点

typedef struct ListNode ListNode;

struct ListNode* middleNode(struct ListNode* head)
{
    ListNode* slow = head;  
    ListNode* fast = head;
    while(fast && fast->next)  //注意这两个判定条件的前后顺序不能变
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

4.获取链表中倒数第k个结点

输入一个链表,输出该链表中倒数第k个结点。

题目链接:牛客 链表中倒数第k个结点.

题解:双指针法

设置两个快慢指针初始均指向链表头结点,快指针提前走k步后,两指针再一起遍历,当快指针指向末尾时,慢指针指向的结点即为倒数第k个结点。

  • 注意判断 k 值是否合法
/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 *
 * C语言声明定义全局变量请加上static,防止重复定义
 */

/**
 * 
 * @param pListHead ListNode类 
 * @param k int整型 
 * @return ListNode类
 */

//双指针法
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    // write code here
    //定义两指针 快指针提前走
    struct ListNode* fast = pListHead;
    struct ListNode* low = pListHead;
    
    //快指针提前走k步
    while(k--)
    {
        if(fast)
        {
            fast = fast->next;
        }
        else //如果fast指向空而k还不为0,说明数据有误;或者是链表为空
        {
            return NULL;
        }
    }
    //两指针一起向后移动直至快指针指向NULL
    //但由于快指针提前走了k步,故这时满指针距结束还有k步
    while(fast)
    {
        fast = fast->next;
        low = low->next;
    }
    return low;
}

5.合并两个有序单链表

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

题目链接:力扣21. 合并两个有序链表.

题解:迭代法

为两有序链表分别设一个遍历指针,再设一个新链表的头结点),遍历指针每次将两链表较小的结点接在新链表头结点后,最后返回头结点的 next 指针即可。

  • 注意两链表均有序
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

typedef struct ListNode ListNode;

//双指针法
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    //如果两链表有一个为空直接返回另一个链表即可
    if(list1 == NULL)
    {
        return list2;
    }
    if(list2 == NULL)
    {
        return list1;
    }

    //创建空链表
    ListNode* newList = (ListNode*)malloc(sizeof(ListNode));
    newList->next = NULL;

    ListNode* cur1 = list1;
    ListNode* cur2 = list2;
    ListNode* cur3 = newList;

    while(cur1 && cur2)
    {
        //由于两链表有序,让新链表指向值小的结点即可
        if(cur1->val <= cur2->val)
        {
            cur3->next = cur1;
            cur1 = cur1->next;
        }
        else
        {
            cur3->next = cur2;
            cur2 = cur2->next;
        }
        cur3 = cur3->next;
    }
    //如果list1没有全部赋值
    if(cur1)
    {
        cur3->next = cur1;
    }
    else  //如果list2没有全部赋值
    {
        cur3->next = cur2;
    }

    return newList->next;
}

6.单链表一分为二

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

题目链接:牛客CM11. 链表分割表.

题解:迭代法

创建两个链表头结点,遍历原链表,按规则拆分原链表至两个新链表,最后合并两个新链表即可。

  • 注意若两链表创建了头结点,合并两链表后,需要释放在堆上开辟的两个头结点
/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/

//创建两个链表分别存放小于x的结点和大于x的结点
//最后将两链表拼接返回即可
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
        //如果该链表为空 直接返回
        if(NULL == pHead)
        {
            return NULL;
        }
        
        struct ListNode* cur = pHead;
        //创建两个带头结点的链表
        struct ListNode* p1Head = (struct ListNode*)malloc(sizeof(struct ListNode));
        p1Head->next = NULL;
        struct ListNode* p2Head = (struct ListNode*)malloc(sizeof(struct ListNode));
        p2Head->next = NULL;
        struct ListNode* p1 = p1Head;
        struct ListNode* p2 = p2Head;
        
        while(cur)
        {
            if(cur->val < x)
            {
                p1->next = cur;
                p1 = p1->next;
            }
            else
            {
                p2->next = cur;
                p2 = p2->next;
            }
            cur = cur->next;
        }
        
        //链接两链表
        p1->next = p2Head->next;
        p2->next = NULL;
        
        //获取表头
        pHead = p1Head->next;
        //释放开辟的两头结点
        free(p1Head);
        free(p2Head);
        
        return pHead;
    }
};

7.判断链表是否为回文结构

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

题目链接:牛客OR36. 链表的回文结构.

题解:迭代法(特殊)

先找到链表中点,再反转链表的后半部分,最后遍历前后部分链表判断两部分是否一样即可。

  • 找链表中值可借用上面的方法采用快慢指针法
  • 逆置链表也可借助上方例子
/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        // write code here
        typedef struct ListNode ListNode;
        //解题思路:先找到链表中点,再反转链表的后半部分,最后遍历前后部分链表
        
        //设置一对快慢指针找中值
        ListNode* fast = A;
        ListNode* slow = A;
        while(fast && fast->next)  //时间复杂度O(N/2)
        {   //这里必须加上对fast->next的判定 因为fast一次走两步
            fast = fast->next->next;
            slow = slow->next;
        }  //循环完成,slow 指向中间结点(若为偶数个指向后半段第一个)
        
        //逆置后半部分 (若为奇数个链表则后半部分包含中值)
        ListNode* cur = slow;
        ListNode* nex = NULL;
        ListNode* pre = NULL;
        while(cur)  //时间复杂度O(N/2)
        {   //依次将结点的next指针指向前一个结点即可
            nex = cur->next;  //暂存cur的下一位地址
            cur->next = pre;  //修改cur的next指针
            pre = cur;  //更新pre指针指向现在节点
            cur = nex;  //cur移向下一位
        }
        
        //前后部分比较
        while(A && pre)  //直接将前半部分用A来代替
        {
            if(A->val != pre->val)
            {
                return false;
            }
            A = A->next;
            pre = pre->next;
        }
        return true;
    }
};

8.判断两链表是否相交

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

题目链接:力扣160. 相交链表.

题解:双指针法(特殊)

先遍历求得两单链表长度,让指向长链表的指针先走(长度差)步,开始同步遍历两指针,若两链表相交,则两指针必定可以相遇。

  • 注意两个链表不能有环结构
  • 链表相交的几种情况:
    在这里插入图片描述
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

typedef struct ListNode ListNode;

//判断两单链表是否有相交 并返回相交结点

//双指针法:

//想法1:从两链表最后一个结点向前比较即可-->?单链表无法向前逆推 故PASS
//想法2:先遍历求得两单链表长度,让指向长链表的指针先走(长度差)步,开始遍历两指针
//返回相同结点即可

int ListSize(ListNode* head)  //求链表长度函数
{
    ListNode* p = head;
    int size = 0;
    while(p)
    {
        size++;
        p = p->next;
    }
    return size;
}

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
    ListNode* cur1 = headA;
    ListNode* cur2 = headB;

    int sizeA = ListSize(headA);
    int sizeB = ListSize(headB);
    int step = 0;
    if(sizeA > sizeB)
    {
        step = sizeA - sizeB;
        while(step--)
        {
            cur1 = cur1->next;
        }
    }
    else
    {
        step = sizeB - sizeA;
        while(step--)
        {
            cur2 = cur2->next;
        }
    }

    //开始指针遍历
    while(cur1)
    {
        if(cur1 == cur2) //若两指针指向的地址相同 说明指向的结点就是相交结点
        {
            return cur1;
        }
        else //若两指针指向不同 依次向下
        {
            cur1 = cur1->next;
            cur2 = cur2->next;
        }
    }

    return NULL;
}

----------------------------------循环链表习题----------------------------------

9.判断链表是否有环结构

给你一个链表的头节点 head ,判断链表中是否有环。

题目链接:力扣141. 环形链表.

题解:快慢指针法(特殊)

快慢指针一起从头结点开始遍历,由于快指针速度快,若链表有环结构,则两指针必定相遇。

  • 注意快慢指针的循环更新设定:slow 指针一次一步,fast 指针一次更新两步
    这样设定是有原因的:
    假设该循环链表的循环周期为T(由T个结点构成环)
    为了使快指针能够与慢指针相遇时不错位,我们需要避免 fast % T = slow
    那么循环的最小周期为 T = 2,所以fast = 2 最合适。
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;

//采用快慢指针法 
//slow指针一次一步、fast指针一次两步
//这样设定是有原因的:
//循环的最小周期为T = 2,我们需要避免 fast % T = slow,否则有可能两指针永远错位
bool hasCycle(struct ListNode *head) 
{
    ListNode* slow = head;
    ListNode* fast = head;
     
    while(fast && fast->next) //注意判定条件的先后顺序
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast) //若快慢指针能相遇说明链表一定存在环结构
        {
            return true;
        }
    }
    return false;
}

10.返回带环链表的入环结点

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

题目链接:力扣142. 环形链表 II.

题解:双指针法(特殊)

假设环长T个结点(周期为T),环前有 L 个结点,入环点到相遇点有 X 个结点
那么在判环时,fast 走了L+X+nT ; slow走了 L+X
且快指针是慢指针速度两倍:L+X+nT = 2(L+X) -> L+X = nT -> L = nT-X
那么,这时让一个指针从头开始,另一个从相遇点开始遍历
当这两指针相遇时,相遇点即为入口点
在这里插入图片描述

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;

//双指针法

//判环
struct ListNode * hasCycle(struct ListNode *head) 
{
    ListNode* slow = head;
    ListNode* fast = head;
     
    while(fast && fast->next) //注意判定条件的先后顺序
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast) //若快慢指针能相遇说明链表一定存在环结构
        {
            return fast;
        }
    }
    return NULL;
}

struct ListNode *detectCycle(struct ListNode *head) 
{
    ListNode* start = head;

    if(hasCycle(head)==NULL)
    {
        return NULL;
    }
    else
    {
        ListNode* entry = hasCycle(head);
    
        while(start != entry)
        {
            start = start->next;
            entry = entry->next;
        }
        return entry;
    }
}

----------------------------------链表进阶习题----------------------------------

11.拷贝带随机指针的链表

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

题目链接:力扣138. 复制带随机指针的链表.

题解:迭代 + 节点拆分

方法:在原链表上拷贝一份链表后拆解下来

  • 【随机指针链表】即在链表的基础上,每个结点增加一个随机指针指向链表中的任意结点(可以是自己或指向NULL),故拷贝时无法直接复制,因为有可能随机指针指向的结点还未拷贝
  1. 赋值每一个结点,并添加在原链表的每个结点之间
  2. 新结点的随机指针指向被复制结点的随机指针指向结点的下一个
  3. 前两步即可得到一份拷贝链表与原链表混合在一起,拆解拷贝链表即可
  4. 注意还原原链表
    在这里插入图片描述
/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */


//随机指针链表拷贝(注意不能改变原链表)
//想法:采用遍历从头依次拷贝
//错误,遍历前面结点的随机指针有可能指向还未复制的后面结点
//所以该题的关键即为如何在能还原链表的前提下改变原链表抵消随机指针的影响

//方法:在原链表上拷贝一份链表后拆解下来
typedef struct Node Node;

struct Node* copyRandomList(struct Node* head) 
{
    //1.拷贝结点并添加在被复制结点的后面
    Node* cur = head;
    while(cur)
    {
        Node* curNext = cur->next;  //备份next指针
        Node* copyNode = (Node*)malloc(sizeof(Node));  //在堆上开辟新节点
        copyNode->val = cur->val;
        copyNode->next = curNext;

        cur->next = copyNode;
        cur = curNext;
    }

    //2.为拷贝结点更新随机指针
    cur = head;
    while(cur)
    {
        //循环结点一次走两个,跳过拷贝节点
        Node* curCopy = cur->next;
        if(cur->random != NULL)  //cur指向的一定是原链表指针
        {
            curCopy->random = cur->random->next;
        }
        else
        {
            curCopy->random = NULL;
        }
        cur = curCopy->next;
    }

    //3.分离拷贝链表
    Node* copyHead = NULL;  //拷贝链表的头指针(注意 原链表头指针有可能为空)
    Node* copyTail = NULL;  //拷贝链表的尾指针【关键】
    cur = head;
    while(cur)
    {
        Node* copy = cur->next;
        Node* copyNext = copy->next;

        if(copyHead == NULL)  //更新拷贝链表头指针
        {
            copyHead = copy;
            copyTail = copy;
        }
        else
        {
            copyTail->next = copy;  //为拷贝链表添加结点
            copyTail = copy;  //更新尾指针
        }

        cur->next = copyNext;  //还原链表
        cur = copyNext;  //循环更新也是一次两步
    }

    return copyHead;
}

12.对链表进行插入排序

给你一个链表的头节点 head ,对链表进行插入排序(从小到大)。

题目链接:力扣147. 对链表进行插入排序.

题解:直接插入排序【own】

插入排序是先假设第一个元素已经有序
从第二个元素开始遍历,将每个元素与有序链表比较并插入合适的位置
难点:链表是单向的,如何将结点插入至合适的有序链表中

  • 注意链表排序的本质是改变结点的next指针
  • 时间复杂度O(N^2),空间复杂度O(1)
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

typedef struct ListNode ListNode;

//链表从小到大排序--直接插入排序

//插入排序是先假设第一个元素已经有序
//从第二个元素开始遍历,将每个元素与有序链表比较并插入合适的位置
//难点:链表是单向的,如何将结点插入至合适的有序链表中
//注意:链表排序的本质是改变结点的next指针

struct ListNode* insertionSortList(struct ListNode* head)
{
    //链表无结点或只有一个结点无需排序
    if(head == NULL || head->next == NULL)
    {
        return head;
    }

    ListNode* ready = head;  //ready指针指向已排好序链表的第一个结点
    ListNode* rTali = head;  //rTail指针指向已排好序链表最后一个结点
    ListNode* await = head->next;  //await指向未排序链表的第一个结点
    
    while(await)
    {
        int flag = 0;  //判断是否需要更新rPrev
        ready = head;  //每次从有序链表的首位开始比较
        ListNode* rPrev = head;  //ready指针的前一位
        while(ready != await)
        {
            if(await->val < ready->val)  //需要放置在ready之前
            {
                rTali->next = await->next;
                await->next = ready;
                if(ready == head)  //若第一个ready就要交换
                {
                    head = await;
                }
                else
                {
                    rPrev->next = await;
                }
                break;
            }
            if(flag>0)
            {
                rPrev = rPrev->next;
            }
            ready = ready->next;
            flag++;
        }
        if(ready == await)  //说明内循环没有插入结点,await在有序链表中最大的
        {
            rTali = await;
        }
        await = rTali->next;  
    }

    return head;
}

13.删除链表重复结点

给你一个链表的头节点 head ,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。

题目链接:牛客JZ76. 删除链表中重复的结点.

题解:双指针法

设置两个指针一前一后,若前后指针值不同继续遍历,若值相同,让前指针继续向后走找到所有值相同结点,再删除即可。

  • 注意遍历循环的限定条件
/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 *
 * C语言声明定义全局变量请加上static,防止重复定义
 */
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 * 
 * @param pHead ListNode类 
 * @return ListNode类
 */

typedef struct ListNode ListNode;

struct ListNode* deleteDuplication(struct ListNode* pHead ) 
{
    // write code here
    //有序链表,那么遍历链表时看后面的指针是否有相同的结点即可
    //双指针遍历法
    
    if(pHead == NULL || pHead->next == NULL)
    {
        return pHead;
    }
    
    ListNode* cur = NULL;  //更新链表指针
    ListNode* first = pHead->next;  //先遍历指针
    ListNode* second = pHead;  //后遍历指针
    
    while(first)
    {
        if(first->val != second->val) //前后结点值不同
        {
            //更新新链表指针
            cur = second;
            //更新遍历指针
            second = first;
            first = first->next;
        }
        else  //前后结点值相同
        {
            //前指针向后遍历处相同结点的最后一个
            while(first && first->val == second->val)  //先判断指针是否存在再向后遍历
            {
                first = first->next;
            }
            //去除相同结点
            if(cur != NULL)  //新链表头指针已分配
            {
                cur->next = first;
            }
            else  //头指针为空 意味着链表的第一个结点就是重复结点
            {
                pHead = first;
            }
            //链表在堆上开辟,需释放舍弃结点
            while(second != first)
            {
                ListNode* backups = second->next;  //备份next地址
                free(second);
                second = backups;
            }
            //更新遍历指针
            if(first == NULL)
            {
                break;
            }
            second = first;
            first = first->next;
        }
    }
    return pHead;
}
;