剑指offer (C++版)分类整理(二):链表类

12 篇文章 2 订阅
本文详细介绍了10道关于链表操作的C++编程题目,包括从尾到头打印链表、寻找链表中倒数第k个结点、反转链表、合并排序链表、复制复杂链表、构建双向链表、找两个链表的第一个公共结点、孩子们的游戏、寻找链表环的入口结点以及删除链表中重复节点的解题思路和方法。
摘要由CSDN通过智能技术生成

1.JZ3 从尾到头打印链表

题目描述:

输入一个链表,按链表从尾到头的顺序返回一个ArrayList。

思路一:如果是从尾到头打印链表,我们自然想到可以使用栈存储节点值,然后倒序打印。

class Solution {
public:
    vector<int> printListFromTailToHead(ListNode* head) {
        vector<int> result;
        stack<int> arr;
        ListNode* p=head;
        while(p!=NULL)
        {
            arr.push(p->val);
            p=p->next;
        }
        while(!arr.empty())
        {
            result.push_back(arr.top());
            arr.pop();
        }
        return result;
    }
};

思路二:可以原地反转链表,然后反转后的顺序打印链表。需要注意的是,原地反转链表需要借助额外两个节点指针,需要特别注意的是节点反转指针的顺序遵从“由后往前”的次序。

class Solution {
public:
    vector<int> printListFromTailToHead(struct ListNode* head) {
        vector<int> res;
        ListNode *cur=head;
        ListNode *pre=cur;
        if(head==NULL)
            return res;
        while(head->next!=NULL){
        	//将cur节点移到pre节点之前
            cur=head->next;
            head->next=cur->next;
            cur->next=pre;
            pre=cur;
        }
        while(cur){
            res.push_back(cur->val);
            cur=cur->next;
        }
        return res;
        
    }
};

2.JZ14 链表中倒数第k个结点

题目描述:

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

思路一:暴力法
先遍历整个链表,统计节点个数n(从1开始),然后从头遍历链表,输出第n-k-1个节点就是结果。

思路二:双指针法
先让一个指针走上k步,此时当前指针距离链表尾部距离为n-k,然后让另一个指针从头开始两个指针同时走,当指针一到达尾部时,指针二距离尾部为n-(n-k)= k,即倒数第k个结点。

class Solution {
public:
    ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
        //p1指针先遍历k个节点,pListHead再从头开始
        ListNode* p1=pListHead;
        for(int i=0;i<k;i++)
            if(!p1)
                return nullptr;
            else
                p1=p1->next;
        while(p1){
            p1=p1->next;
            pListHead=pListHead->next;
        }
        return pListHead;
    }
};

3.JZ15 反转链表 链表

题目描述:

输入一个链表,反转链表后,输出新链表的表头。

思路一:原地反转。与第一题非常类似,只是输出反转后的表头。

class Solution {
public:
    ListNode* ReverseList(ListNode* pHead) {
        if(pHead == NULL || pHead->next == NULL)
        {
            return pHead;
        }
        ListNode* pre = NULL;
        ListNode* after = NULL;
        while(pHead)
        {
            //此处顺序需要弄清楚,先后再前
            after = pHead->next; //after最多为空
            pHead->next = pre;
            pre = pHead;
            pHead = after;
        }
        return pre;
    }
};

思路二:使用栈存储结点,利用栈的先入后出顺序。与第一题非常类似,只是输出反转后的表头。

class Solution {
public:
    ListNode* ReverseList(ListNode* pHead) {
        if(pHead == NULL || pHead -> next == NULL)
            return pHead;
        ListNode* p = pHead;
        stack<ListNode* > s;  //链表类型的栈
        while(p -> next)
        {
            s.push(p);
            p = p ->next;
        }
        ListNode* head = p;  //原链表的最后指针p,即新反序链表头指针,用head储存
        while(!s.empty())
        {
            p -> next = s.top();
            p = p ->next;  //移动
            s.pop();
        }
        p -> next = NULL;
        return head;
    }
};

4.JZ16 合并两个排序的链表

题目描述:

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

思路:先判断链表是否为空,为空就返回另一个。新建一个表头,其下一个结点是两个链表中的较小结点,以此循环直到一个链表结束。然后判断两个链表是否结束,没结束则直接添加到新链表尾部,最后返回的是新表头的下一结点。

class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
    {
     if(!pHead1)
         return pHead2;
     if(!pHead2)
         return pHead1; //若有某一个链表为空,则返回另一个
     ListNode *p=new ListNode(0);
     ListNode *q=p;//两个指针都指向合成后新链表头节点,p用来做插入,q用来保存头结点地址
     while(1)
     {
         if(pHead1->val < pHead2->val)
         {
             p->next = pHead1; //如果L1值小,则L1向下走
             pHead1 = pHead1->next;
          }
         else
         {
             p->next = pHead2; //如果L2值小,则L2向下走
             pHead2 = pHead2->next;
         }
         p = p->next;
         if(pHead1==NULL||pHead2==NULL) //若某个链表走完,则跳出
             break;
     }
     if(!pHead1)
         p->next = pHead2;
     if(!pHead2)
         p->next = pHead1; //若某个链表走完,则将另一个链表续在合成后链表的末尾
     return q->next;
    }
};

5.JZ25 复杂链表的复制

题目描述:

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

思路一:对于一个单链表的复制比较简单,但是对于复杂链表,我们可以先复制普通next指针的链表,然后处理random。本方法默认节点值不重复。

class Solution {
public:
    RandomListNode* Clone(RandomListNode* pHead)
    {
        if(pHead == nullptr)
            return pHead;
        auto p1 = pHead;  //保存头结点
        auto head = new RandomListNode(pHead->label); //新建表头
        auto p2 = head;  //p遍历,q保存表头
        while(p1 != nullptr)
        {
            //遍历原链表,建立下一节点
            p1 = p1->next;
            if(p1)
            {    
                p2->next = new RandomListNode(p1->label);
                p2 = p2->next;
            }
            else    p2->next = NULL;
        }
        p1 = pHead;
        p2 = head;
        while(p1 != nullptr)
        {
            //遍历原链表,建立随机节点
            if(p1->random != nullptr)
            {
                //存在random节点,设置从头开始遍历,直到找到random的值相同的节点,默认节点值不重复
                auto l = pHead;
                auto r = head;
                while(l->label != p1->random->label)
                {
                    r = r->next;
                    l = l->next;
                }
                p2->random = r;
            }
            p1 = p1->next;
            p2 = p2->next;
        }
        return head;
    }
};

思路二:原地复制,每一个结点后都复制一个与原节点相同的节点,对于random,只需要指向random->next即可。

/*
struct RandomListNode {
    int label;
    struct RandomListNode *next, *random;
    RandomListNode(int x) :
            label(x), next(NULL), random(NULL) {
    }
};
*/
class Solution {
public:
    RandomListNode* Clone(RandomListNode* pHead)
    {
        if(pHead == NULL)    return NULL;
        RandomListNode* head = pHead;
        //复制
        while(head){
            RandomListNode* cloneNode = new RandomListNode(head->label);
            cloneNode->next = head->next;
            head->next = cloneNode;
            head = cloneNode->next;
        }
        head = pHead;
        //处理复杂指针
        while(head){
            if(head->random){
                head->next->random = head->random->next;
            }
            head = head->next->next;
        }
        //拆分链表
        RandomListNode* clonehead = pHead->next;
        head = pHead;
        while(head){
            RandomListNode* cloneNode = head->next;
            head->next = cloneNode->next;
            //注意cloneNode->next可能为空
            cloneNode->next = cloneNode->next == NULL?NULL:cloneNode->next->next;
            head = head->next;
        }
        return clonehead;
    }
};

6.JZ26 二叉搜索树与双向链表

题目描述:

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向

思路:对于一棵二叉搜索树,其左子树上所有的点小于根节点,右子树节点大于根节点,其子树也是相同。中序遍历即返回二叉搜索树的递增排序。将二叉搜索树中序遍历的结果存入数组中,然后对数组进行双向链表重建。

class Solution {
public:
    TreeNode* Convert(TreeNode* pRootOfTree)
    {
        //二叉搜索树:左子树上所有的点小于根节点,右子树节点大于根节点,其子树也是相同
        //中序遍历即返回二叉搜索树的递增排序
        vector<TreeNode* > arrayTree;  //储存树节点的数组
        if(pRootOfTree)  //中序遍历所有节点
        {
            Mid(pRootOfTree,arrayTree);
        }
        else
            return NULL;
        for(int i = 0; i<arrayTree.size();i++)
        {
            if(i == 0)
            {
                arrayTree[i]->left == NULL;
                if(i+1 <= arrayTree.size()-1)
                    arrayTree[i]->right=arrayTree[i+1];
                else    //尾结点
                    arrayTree[i]->right=NULL;
            }
            else if(i==arrayTree.size()-1)
            {
                arrayTree[i]->right == NULL;
                if(i-1>=0)
                    arrayTree[i]->left=arrayTree[i-1];
                else    //头结点
                    arrayTree[i]->left=NULL;
            }
            else
            {
                arrayTree[i]->left=arrayTree[i-1];
                arrayTree[i]->right=arrayTree[i+1];
            }
        }
        return arrayTree[0];  //头结点
    }
private:
    void Mid(TreeNode* pRootOfTree,vector<TreeNode* > &arrayTree)  //中序遍历递归
    {
        if(pRootOfTree == NULL)
            return;
        Mid(pRootOfTree->left,arrayTree);
        arrayTree.push_back(pRootOfTree);
        Mid(pRootOfTree->right,arrayTree);
    }
};

7.JZ36 两个链表的第一个公共结点

题目描述:

输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)

思路一:有公共结点,那么分别统计两个链表的长度,让长的链表先走一部分,使得剩下的部分与短的链表长度相等,随后一一比较,找到公共结点,返回即可。

class Solution {
public:
    ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
        //两个链表在第一个公共节点后使用公共的节点
        ListNode* p1 = pHead1;
        ListNode* p2 = pHead2;
        int len1 = 0,len2 = 0,diff;  //两个链表的长度
        while(p1){
            len1++;
            p1 = p1->next;
        }
        while(p2){
            len2++;
            p2 = p2->next;
        }
        if(len1 > len2){
            diff = len1 - len2;
            p1 = pHead1;
            p2 = pHead2;
        }
        else{
            diff = len2 - len1;
            p1 = pHead2;
            p2 = pHead1;
        }
        for(int i = 0;i<diff;i++){
            //两链表剩下的节点数相等
            p1 = p1->next;
        }
        while(p1!=NULL&&p2!=NULL){
            if(p1 == p2)
                break;
            p1 = p1->next;
            p2 = p2->next;
        }
        return p1;
    }
};

思路二:有公共结点,我们要保证两个链表剩下的部分一样长,才可能找到公共结点。可以分别遍历两个链表,一个走完后将其next等于另一个链表的表头,这样两个链表一样长

class Solution {
public:
    ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
        if(pHead1==NULL || pHead2==NULL)    return NULL;
        ListNode* p1 = pHead1;
        ListNode* p2 = pHead2;
        while(p1 != p2){
            if(p1 == NULL)    p1 = pHead2;
            if(p2 == NULL)    p2 = pHead1;
            if(p1 != p2){
                p1 = p1->next;
                p2 = p2->next;
            }
        }
        return p1;
    }
};

8.JZ46 孩子们的游戏(圆圈中最后剩下的数)

题目描述:

每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
如果没有小朋友,请返回-1

思路一:数学法

class Solution {
public:
    int LastRemaining_Solution(int n, int m)
    {
        if(n==0) 
            return -1;
        
       int s=0;
       for(int i=2;i<=n;i++){
           s=(s+m)%i;
       }
       return s;
    }
};

思路二:数组模拟法。利用数组模拟环,记录每次出去的下标并将其置为-1,循环时遇到值为-1的直接跳过,最后那个值不为-1的即是结果。

class Solution {
public:
    int LastRemaining_Solution(int n, int m)
    {
        
        if(n<1 || m<1) return -1;
        vector<int> array(n,0);
        int i = -1,step = 0, count = n;
        while(count>0){   //跳出循环时将最后一个元素也设置为了-1
            i++;          //指向上一个被删除对象的下一个元素。
            if(i>=n) i=0;  //模拟环。
            if(array[i] == -1) continue; //跳过被删除的对象。
            step++;                     //记录已走过的。
            if(step==m) {               //找到待删除的对象。
                array[i]=-1;
                step = 0;
                count--;
            }        
        }
        return i;//返回跳出循环时的i,即最后一个被设置为-1的元素
    }
};

9.JZ55 链表中环的入口结点

题目描述:

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

解析:
龟兔赛跑
链表头到环入口长度为–a
环入口到相遇点长度为–b
相遇点到环入口长度为–c

则:相遇时
快指针路程=a+(b+c)k+b ,k>=1 其中b+c*为环的长度,k为绕环的圈数(k>=1,即最少一圈,不能是0圈,不然和慢指针走的一样长,矛盾)。
慢指针路程=a+b
**快指针走的路程是慢指针的两倍,所以:(a+b)2=a+(b+c)k+b
化简可得:
a=(k-1)(b+c)+c
这个式子的意思是: 链表头到环入口的距离 = 相遇点到环入口的距离+(k-1)圈环长度。其中k>=1,所以k-1>=0圈。所以两个指针分别从链表头和相遇点出发,最后一定相遇于环入口。

思路:经典的快慢指针(龟兔赛跑问题),具体推理如上。

class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead)
    {
        //龟兔赛跑,如果链表中环有n个结点,fast在链表上向前移动n步
        //然后两个节点以相同的速度向前移动
        //最终会在入口处相遇,即求出发点到入口的长度
        if(pHead==NULL||pHead->next==NULL)
            return NULL;
        ListNode* fast=pHead;
        ListNode* slow=pHead;
        while(fast && fast->next)
        {
            fast=fast->next->next;
            slow=slow->next;
            //相遇
            if(fast==slow)
            {
                fast=pHead;
                break;
            }
            if(fast == NULL)    return NULL;
        }
        while(fast!=slow)
                {
                fast=fast->next;
                slow=slow->next;
            }
        return fast;
    }
};

10.JZ56 删除链表中重复的结点

题目描述:

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

思路:要跳过重复节点,如果表头是重复节点的话,需要跳过表头,不方便存储新链表的表头。于是我们新建一个表头结点,在遇到新节点时,判断其后面节点是否与当前节点相等,相等就跳过,否则就添加到新链表的后面。

class Solution {
public:
    ListNode* deleteDuplication(ListNode* pHead)
    {
        ListNode *result = new ListNode(0);  //新建链表,result保存表头
        ListNode *res = result;  //待存储节点
        while(pHead && pHead->next)
        {
            if(pHead->val == pHead->next->val)
            {
                while(pHead->next && pHead->val == pHead->next->val)
                    pHead = pHead->next;
            }
            else{
                res->next = pHead;
                res = res->next;
            }
            pHead = pHead->next;
        }
        res->next = pHead; 
        
        return result->next;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值