链表题目练习

在写数据结构相关题目时我们一定要去画图!!!

下面的题没有画图,先看题画图没思路再看思路,我第一遍也不会,多写题就会好很多!

单向链表题目练习

1、反转链表

给出链表节点头head,请你反转链表

比如:1 2 3 4 5->5 4 3 2 1

那头就指向NULL,那我们想要得到下一个结点就得事先将下一个节点保存起来。

那我们来分析一下。

要反转,首先我们要确定谁要指向谁,下一个结点为多少。需要三个变量,开始n1=NULL,n2=head,n3=head->next,这样我们就具备了移位的所有变量。代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head) {
    //防止链表本身1为空
    if(head==NULL)
    {
        return NULL;
    }

    struct ListNode* n1= NULL;
    struct ListNode* n2= head;
    struct ListNode* n3= head->next;
    //n1,n2,n3向后移动一位
    while(n2)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n3)
            n3 = n3->next;
    }
    return n1;
}

 当n2为5的结点时,n3为空,因此下一次不能让n3访问空加一个if判断。当n2等于空时,恰好n1是链表的头。

 2、链表的中间节点

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

思路:快慢指针,找中间的话慢指针一次走一步,快指针一次走两步,当快指针为空时,慢指针的值就是链表的中间节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* fast=head;
    struct ListNode* slow=head;
    while(fast->next != NULL)
    {
        slow = slow->next;
        fast =fast->next;
        if(fast->next==NULL)//防止对访问空指针
            break;
        else
            fast =fast->next;

    }
    return slow;
    
}

3、链表中倒数第K个节点

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

输入1,{1,2,3,4,5}返回值5。

思路:我们可以使用先后行指针,fast为先行指针,slow为后行指针。

先让先指针走k次再让先后指针一起走直到先指针为空时,后指针就是倒数第k个节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* middleNode(struct ListNode* head,int k) {
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while(k--)
    {
        //k还没减到0,fast就为空那倒数值大于链表元素个数
        if(fast == NULL)
        {
            return NULL;
        }
        fast = fast->next;
    }
    while(fast)
    {
        fast = fast->next;
        slow = slow->next;
    }
    return slow;
}

 4、合并两个有序链表合成后的新链表也是有序的

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

思路:我们要返回新链表的头那我们肯定要定义一个头来保存新链表的头,链表要有头有尾,

我们进行list1,list2的数值进行比较,谁小我们就把尾指向谁。list1,list2往后移,谁先为空,让尾指向另一个。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    if (list1 == NULL)
        return list2;
    if (list2 == NULL)
        return list1;
    struct ListNode* head = NULL;
    struct ListNode* tail = NULL;
    // 先取一个小的当做头
    if (list1->val > list2->val) 
    {
        head = tail = list2;
        list2 = list2->next;
    } else 
    {
        head = tail = list1;
        list1 = list1->next;
    }
    // 开始排序
    // 排序的结束的条件是什么?
    // 结束条件是当list1或list2节点为空时结束
    while (list1 && list2) {
        if (list1->val > list2->val) 
        {
            //tail->next = list2;
            //list2 = list2->next;
            //tail = tail->next;
            tail->next = list2;
            tail = list2;
            list2 = list2->next;
            

        } else
        {
            tail->next = list1;
            list1 = list1->next;
            tail = tail->next;
            //tail->next = list1;
            //tail = list1;
            //list1 = list1->next;
          
        }
    }
    // 如果list1节点先为空那就让末节点指向list2
    if (list1) 
    {
        tail->next = list1;
    }
    if (list2) 
    {
        tail->next = list2;
    }
    return head;
}

5、链表分割

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

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

示例 1:

输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]

示例 2:

输入:head = [2,1], x = 2
输出:[1,2]

思路:我们可以新建两个带哨兵位的链表一个用来存放比x小,一个用来存放大于等于x。然后将小链表的尾指向大链表的头。带哨兵位的链表一定要让下一个结点指向空。对链表进行遍历操作时我们需要创建一个指针从链表头移位到链表尾,移动链表头是一件很奇怪的事。

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


struct ListNode* partition(struct ListNode* head, int x){
    //先判断链表中是否有节点
    if(head==NULL)
    {
        return NULL;
    }
    //新建两个链表头(哨兵位)
    struct ListNode* list1 = (struct ListNode*)malloc(sizeof(struct ListNode));
    list1->next = NULL;//这一句无所谓,list1的尾要指向list2
    struct ListNode* list2 = (struct ListNode*)malloc(sizeof(struct ListNode));
    list2->next = NULL;//如果链表全是小于x,最后链接时要保证list1的尾指向空
    struct ListNode* tail = head;
    struct ListNode* tail1 = list1;
    struct ListNode* tail2 = list2;
    while(tail)
    { 
        //判断然后进行放置     
        if(tail->val < x)
        {
            tail1->next=tail;
            tail1=tail;
        }
        else
        {
            tail2->next = tail;
            tail2 = tail;
        }
        //对原链表遍历走一步
         tail=tail->next;
    }
    //链接两个链表
    tail1->next=list2->next;
    tail2 ->next= NULL;
    
    //记得释放掉哨兵位的结点
    struct ListNode* newhead = list1->next;
    free(list1);
    free(list2); 
    return newhead;
}

6、链表的回文结构

如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。

示例 1:

输入: head = [1,2,3,3,2,1]
输出: true

示例 2:

输入: head = [1,2]
输出: false

思路1:我们可以新建一个链表然后将新的链表进行逆置(反转),如果链表的每个元素个逆置后的链表相同,那该链表就是回文结构。空间复杂度为O(N),时间复杂度为O(N)

bool isPalindrome(struct ListNode* head){

    struct ListNode* tail = head;
    struct ListNode* BuildNewHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* NewHead = BuildNewHead;//保存新建链表的头
    struct ListNode* NewTail =  BuildNewHead ;   
    //复制原链表
    while(tail)
    {
        //新建一个链表       
        NewTail->val = tail->val; 
        tail = tail->next; 
        //这里为什么还要进行判断?
        //当tail更新后可能就为空了,就不需要再次创建新的结点
        if(tail)
        {
        BuildNewHead = (struct ListNode*)malloc(sizeof(struct ListNode));   
        NewTail->next =  BuildNewHead;
        NewTail = BuildNewHead;
        }
        //那把tail = tail->next; 放到循环最后是否可行?
        //我们在循环外创建好一个结点后在进入循环保存,这就导致每次最后多了一个节点,
        //因此这个if是为了避免了多余节点的出现。             
    }
    NewTail->next =NULL;
    //逆置链表
    struct ListNode* front = NULL;
    struct ListNode* middle = NewHead;
    struct ListNode* end = NewHead->next;
    while(middle)
    {
        middle->next = front;
        front = middle;
        middle = end;
        if(end)
            end = end->next;
    }
    tail = head;
    NewTail = front;
   while(tail)
    {
       if(tail->val == NewTail->val)
        {
            tail=tail->next;
            NewTail=NewTail->next;
        }
        else
        {
            return false;
        }
    }
    return true;
}

如果要求空间复杂度为O(1),时间复杂度为O(N)呢?

方法二:我们先求出链表的长度,让中间之后的链表进行逆置,在使用等距指针进行比较

大家可以试一下:

7、相交链表

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

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

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

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

思路:我们计算A比B长的绝对值:让长的指针先走长度差个节点,再一起走,等到两个节点相同就是交点。否则就没有交点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode * tailA = headA;
    struct ListNode * tailB = headB;
    //计算AB链表的长度差
    int LenA=0;
    int LenB=0;
    int Delen;
    while(tailA)
    {
        LenA++;
        tailA = tailA->next;
    }
    while(tailB)
    {
        LenB++;
        tailB = tailB->next;
    }
    tailA = headA;
    tailB = headB;
    Delen = abs(LenA-LenB);
    //谁长谁长谁先走
    if(LenA>LenB)
    {
        while(Delen--)
        {
            tailA=tailA->next;
        }
    }
    else
    {
      while(Delen--)
        {
            tailB=tailB->next;
        }  
    }
    //一起走并比较
    while(tailA&&tailB)
    {
        if(tailA == tailB)
        {
            return tailA;
        }
        tailA = tailA->next;
        tailB = tailB->next;
    }
    return NULL;
}

8、环形链表

给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null

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

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

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

方法一:

整体思路,我们可以定义一个快慢指针,快指针一次走两步,慢指针一次走一步,都从head开始走,那在环中快指针能否恰好追上慢指针?答案是一定可以(之后进行分析),那我们就可以使用在快指针为空之前判断是否快慢指针相等,如果相等那就有环。还有怎么去找这个入环口呢?

分析:

之后分析我就以这个图为标准了。慢指针第一次进入环时,假设慢指针与快指针相差N个节点,因为快指针每次走两步,慢指针走一步,他两每走一次相差的距离为:N-1,N-2,N-3·····,1,0相遇。所以快指针走两步是一定可以和慢指针相遇的!那快指针一次走三部呢?

快指针一次走三步分析:

当N为偶数时:N-2,N-4,N-6,······,2,0->可以相遇

当N为奇数时:N-2,N-4,N-6,······,1,-1。那之后能否恰好相遇取决于什么呢?取决于环节点的个数,如果环节点 为偶数个,偶数-1又是奇数(),那之后就不可能恰好相遇了。反之环节点个数为奇数个就能恰好相遇。

快指针一次走四步呢?那就分为N是否是3的倍数,下一次是否也是三的倍数?可以自行分析。

那么是否有环的代码我们就可以这样写

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    //1、判断链表是否有环
    //快指针一次走两步,慢指针一次走一步
    while(fast&&fast->next)//防止 fast->next->next访问空指针
    {

        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)//有环,返回链表头
        {
           return head;
        }
    } 
    return NULL;//无环,返回空
}

如何找到入环的结点呢?这里提供两个思路。

思路一:

 根据上面的图我们分析一下:从头开始,和从环那个地方开始每次走一步,恰好两个指针都走到入环节点。那会到求距离相等问题了:

快指针走的路程是慢指针的两倍,那快指针走的路程就可以列一个等式:2(X+L)=n*c+L+X

L=nc-X,那nc-X就是图中恰好相遇的点meet。那我们只要两个指针head,meet一起走,等他们相等时就是入环的节点

代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    //1、判断链表是否有环
    //快指针一次走两步,慢指针一次走一步
    while(fast&&fast->next)//防止 fast->next->next访问空指针
    {

        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)//有环
        {
            //找环入口
            struct ListNode* meet = slow;
            while(meet !=head)
            {
                meet=meet->next;
                head=head->next;
            }
            return meet;
        }
    } 
    return NULL;
}

 思路二:

当快慢指针恰好相遇时,我们让相遇的结点指向空,(思考一下,这是不是就成了相交链表找交点的问题了)。

9、复杂链表的赋值

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

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例 3:

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

题目解析:该链表除了一个next,还有一个random,这个random会随机指向链表中的任意一个结点,我们需要复制这个链表,复制的这个链表不能和原链表有任何重叠。

暴力思路:先复制一个相同的链表,再对这个原链表每一个节点进行遍历,找到对应的random与起始节点的距离,在新链表中根据距离找到节点的random。

这里我学习大佬的思路:我们将复制链表的每个节点,插在原链表的后面,会发现什么呢?原链表的节点指向NULL那复制的节点也指向NULL。如果原链表的random不指向NULL,那复制结点的random就等于原节点random->next看下面这张图:

 然后再把复制的链表取下来。

第一步:新建链表并将每个结点插在原链表的后面

struct Node* copyRandomList(struct Node* head) {
        
        //将新建链表节点插在原链表节点的后面         
        struct Node* n0 = head;                
         while(n0)         
         {             
            struct Node* BuildNewHead = (struct Node*)malloc(sizeof(struct Node));//新建节点 
            BuildNewHead->val= n0->val;  
             //开始插入
            BuildNewHead->next = n0->next;//BuildNewHead->n1             
            n0->next =   BuildNewHead;                           
            n0 = BuildNewHead->next; //也可以这样写n0 = n0->next->next       
         }        

第二步:遍历原链表,让复制新链表的结点找到自己的结点

原链表的节点指向NULL那复制的节点也指向NULL。如果原链表的random不指向NULL,那复制结点的random就等于原节点random->next

          n0 = head;           
          while(n0)         
          {             
            if(n0->random == NULL)             
            {                 
                n0->next->random = NULL;             
            }             
            else             
            {                 
                n0->next->random = n0->random->next;              
            }
            //遍历跳过复制结点所以走两步             
            n0 = n0->next->next;         
         }         

第三步将复制的链表拆下来。

        //拆解新链表,恢复原链表          
         n0 = head;                 
        struct Node* cophead = NULL,*copytail = NULL;
        while(n0)
        {
            struct Node* copy = n0->next;//定义一个变量用于标记要复制哪一个结点,第一个复制结点是链表的第二个结点
            struct Node* next = copy->next;//原链表前一个指向下一个的节点
            if(copytail ==NULL)//如果链表没有结点就头插
            {
              cophead = copytail =  copy; 
            }
            else//否则就尾插
            {
                copytail->next = copy;
                copytail =copy;
            }
            n0->next = next;//恢复原链表
            n0 = next;//在原链表走一步,实际走两步
        }
      
        return   cophead;     
	
}

我第一次也写了一个但是一直出问题:前面两步都一样,第三步我是这么写的,有错误,但现在不知道在哪错了

        struct Node* n2 = head->next->next;          
         n0 = head;         
         n1 = head->next; 
         struct Node* cophead = n1;         
        while(n2)
        {
            n0->next = n2;
            n1->next = n2->next;
            n0 = n2;
            n1 = n2->next;
            n2 = n1->next;
            if(n2 = NULL)
            {
                n0->next = n2;          
                n1->next = NULL;
            }
        }
      
        return   cophead;
} 

  • 23
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
A:好的,下面是K - 双向链表练习题的解答。 题目描述: 设计一个数据结构,要求实现双向链表的基本操作:插入、删除、反转等,并实现一个方法向链表中添加一个元素。同时,该数据结构需要支持以下操作: 1. get(k):返回链表中第 k 个节点的值。如果 k 小于 1 或大于链表的长度,则返回 -1。 2. addAtHead(val):在链表的头部插入一个值为 val 的节点。 3. addAtTail(val):在链表的尾部插入一个值为 val 的节点。 4. addAtIndex(index,val):在链表中的第 index 个节点前插入一个值为 val 的节点。如果 index 等于链表的长度,则在链表的末尾添加该节点。如果 index 大于链表的长度,则不会插入。 5. deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。 解题思路: 题目要求我们设计一个双向链表数据结构,并提供选定的一些操作。这是一个经典的链表问题,我们需要先设计链表节点的结构体,并在节点中保留前驱和后继指针,便于双向操作。然后,我们可以定义一个链表类,在该类中定义一些方法来实现链表的基本操作。 本题需要实现的方法包括: 1. get(k):遍历链表,找到第 k 个节点并返回该节点的值。 2. addAtHead(val):创建一个新节点,将该节点作为头节点,并更新头节点的前驱指针。 3. addAtTail(val):创建一个新节点,将该节点作为尾节点,并更新尾节点的后继指针。 4. addAtIndex(index,val):遍历链表,找到第 index - 1 个节点,创建一个新节点,并将其插入到该节点的后面。如果 index 为零,则将新节点插入到头部。如果 index 等于链表的长度,则将新节点插入到末尾。 5. deleteAtIndex(index):遍历链表,找到第 index - 1 个节点,并将其后继指针指向第 index + 1 个节点。如果 index 为零,则更新头节点。如果 index 等于链表的长度 - 1,则更新尾节点。 代码实现: 下面是基于C++的实现代码,其中Node是一个链表节点的结构体,List是链表类的定义: ```cpp #include<iostream> using namespace std; // 链表节点结构体 struct Node { int val; // 节点的值 Node* pre; // 前驱指针 Node* nxt; // 后继指针 Node(int _val):val(_val),pre(nullptr),nxt(nullptr){} // 构造函数 }; // 链表类 class List{ private: Node* head; // 头节点 Node* tail; // 尾节点 int size; // 链表长度 public: List():head(nullptr),tail(nullptr),size(0){} // 构造函数 int get(int k){ if(k < 1 || k > size) // 判断k是否合法 return -1; Node* p = head; for(int i=1; i<k; i++) // 遍历链表,找到第k个节点 p = p->nxt; return p->val; // 返回节点的值 } void addAtHead(int val){ Node* p = new Node(val); // 创建新节点 if(size == 0){ // 链表为空的情况 head = p; tail = p; }else{ // 链表非空的情况 p->nxt = head; // 插入节点 head->pre = p; head = p; } size++; // 更新链表长度 } void addAtTail(int val){ Node* p = new Node(val); // 创建新节点 if(size == 0){ // 链表为空的情况 head = p; tail = p; }else{ // 链表非空的情况 tail->nxt = p; // 插入节点 p->pre = tail; tail = p; } size++; // 更新链表长度 } void addAtIndex(int index, int val){ if(index > size) // index不合法,不插入 return; if(index <= 0) // 如果index小于等于0,插入到头部 addAtHead(val); else if(index == size) // 如果index等于size,插入到尾部 addAtTail(val); else{ // 如果index在链表中 Node* p = head; for(int i=1; i<index; i++) // 找到第index-1个节点 p = p->nxt; Node* q = new Node(val); // 创建新节点 q->nxt = p->nxt; // 插入节点 p->nxt->pre = q; p->nxt = q; q->pre = p; size++; // 更新链表长度 } } void deleteAtIndex(int index){ if(index < 0 || index >= size) // index不合法,不删除 return; if(index == 0){ // 如果要删除的是头节点 head = head->nxt; // 更新头节点 if(head == nullptr) // 如果链表为空,尾节点也需要更新 tail = nullptr; else head->pre = nullptr; }else if(index == size-1){ // 如果要删除的是尾节点 tail = tail->pre; // 更新尾节点 tail->nxt = nullptr; }else{ // 如果要删除的是中间节点 Node* p = head; for(int i=1; i<index; i++) // 找到第index-1个节点 p = p->nxt; p->nxt = p->nxt->nxt; // 删除节点 p->nxt->pre = p; } size--; // 更新链表长度 } }; int main(){ List l; l.addAtHead(1); l.addAtTail(3); l.addAtIndex(1,2); // 链表变为[1,2,3] cout<<l.get(1)<<" "; // 返回2 l.deleteAtIndex(1); // 现在链表是[1,3] cout<<l.get(1)<<" "; // 返回3 return 0; } ``` 总结: 双向链表实现相对较多的语言,相对单向链表更适合一些场景;比如说LUR Cache。对于双向链表的实现,我们需要注意节点间指针的指向关系,以及头节点和尾节点的处理。感兴趣的读者可以继续尝试其他链表问题,如链表的分割、链表的反转等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值