剑指offer刷题记录

本文介绍了四种链表操作:从尾到头打印链表、反转链表、合并两个排序链表以及寻找链表环的入口节点。针对每种操作,提供了详细的解题思路和代码实现,包括使用双指针、迭代和哈希表等方法。这些方法都注重在保持空间复杂度O(1)或O(n)的同时,实现高效的时间复杂度。
摘要由CSDN通过智能技术生成

剑指offer刷题记录(1)(2022.02.18)

写在前面:没有学过c++,参考解题评论边刷题边学。

数据结构
  1. 从尾到头打印链表

描述
输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。
如输入{1,2,3}的链表
返回一个数组为[3,2,1]
0 <= 链表长度 <= 10000

解法: 直接遍历
题目很简单,很朴素。我们直接从这个链表的头节点开始进行遍历。然后我们记录下这个数组的每个节点的信息。最后反转一下整个数组,返回即可。
代码如下
/**
*  struct ListNode {
*        int val;
*        struct ListNode *next;
*        ListNode(int x) :
*              val(x), next(NULL) {
*        }
*  };
*/
class Solution {
public:
    vector<int> printListFromTailToHead(ListNode* head) {
        vector<int> ans;
        while(head){
            ans.push_back(head->val);
            head=head->next;
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }
};

需要直接遍历长度为n的链表的所有的结点,时间复杂度为O(n)
需要存储长度为n的链表的所有的结点,空间复杂度为O(n)
  1. 反转链表

描述
给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
数据范围: 0≤n≤1000
要求:空间复杂度 O(1)O(1) ,时间复杂度 O(n)O(n) 。
如当输入链表{1,2,3}时,
经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。

此题想考察的是:如何调整链表指针,来达到反转链表的目的。
初始化:3个指针
1)pre指针指向已经反转好的链表的最后一个节点,最开始没有反转,所以指向nullptr
2)cur指针指向待反转链表的第一个节点,最开始第一个节点待反转,所以指向head
3)nex指针指向待反转链表的第二个节点,目的是保存链表,因为cur改变指向后,后面的链表则失效了,所以需要保存
接下来,循环执行以下三个操作
1)nex = cur->next, 保存作用
2)cur->next = pre 未反转链表的第一个节点的下个指针指向已反转链表的最后一个节点
3)pre = cur, cur = nex; 指针后移,操作下一个未反转链表的第一个节点
循环条件,当然是cur != nullptr
循环结束后,cur当然为nullptr,所以返回pre,即为反转后的头结点

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* ReverseList(ListNode* pHead) {
        ListNode* cur=pHead;
        ListNode* next=nullptr;
        ListNode* pre=nullptr;
        while(cur){
            next=cur->next;
            cur->next=pre;
            pre=cur;
            cur=next;
        }
        return pre;
    }
};
  1. 合并两个排序的链表

描述
输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
数据范围: 0≤n≤1000,-1000≤节点值≤1000
要求:空间复杂度 O(1),时间复杂度 O(n)
如输入{1,3,5},{2,4,6}时,合并后的链表为{1,2,3,4,5,6},所以对应的输出为{1,2,3,4,5,6}

迭代法:
初始化:定义cur指向新链表的头结点
操作:
如果l1指向的结点值小于等于l2指向的结点值,则将l1指向的结点值链接到cur的next指针,然后l1指向下一个结点值
否则,让l2指向下一个结点值
循环步骤1,2,直到l1或者l2为nullptr
将l1或者l2剩下的部分链接到cur的后面
技巧
一般创建单链表,都会设一个虚拟头结点,也叫哨兵,因为这样每一个结点都有一个前驱结点。
/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        ListNode* vhead = new ListNode(-1);
        ListNode* cur = vhead;
        while(pHead1 && pHead2){
            if(pHead1->val<=pHead2->val){
                cur->next=pHead1;
                pHead1=pHead1->next;
            }
            else{
                cur->next=pHead2;
                pHead2=pHead2->next;
            }
            cur=cur->next;
        }
        cur->next=pHead1?pHead1:pHead2;
        return vhead->next;
    }
};
  1. 两个链表的第一个公共结点

描述
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
数据范围: 0≤1000
要求:空间复杂度 O(1),时间复杂度O(n)

输入描述:
输入分为是3段,第一段是第一个链表的非公共部分,第二段是第二个链表的非公共部分,第三段是第一个链表和二个链表的公共部分。 后台会将这3个参数组装为两个链表,并将这两个链表对应的头节点传入到函数FindFirstCommonNode里面,用户得到的输入只有pHead1和pHead2。
返回值描述:
返回传入的pHead1和pHead2的第一个公共结点,后台会打印以该节点为头节点的链表。
输入:{1,2,3},{4,5},{6,7}
输出:{6,7}
说明:第一个参数{1,2,3}代表是第一个链表非公共部分,第二个参数{4,5}代表是第二个链表非公共部分,最后的{6,7}表示的是2个链表的公共部分
这3个参数最后在后台会组装成为2个两个无环的单链表,且是有公共节点的

在这里插入图片描述

双指针法
这里先假设链表A头结点与结点8的长度 与 链表B头结点与结点8的长度相等,那么就可以用双指针。
初始化:指针ta指向链表A头结点,指针tb指向链表B头结点
如果ta == tb, 说明找到了第一个公共的头结点,直接返回即可。
否则,ta != tb,则++ta,++tb

所以现在的问题就变成,如何让本来长度不相等的变为相等的?
假设链表A长度为a, 链表B的长度为b,此时a != b
但是,a+b == b+a
因此,可以让a+b作为链表A的新长度,b+a作为链表B的新长度。
这样,长度就一致了,可以用上述的双指针解法了。


/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
        ListNode* a=pHead1;
        ListNode* b=pHead2;
        while(a!=b){
            a= a ? a->next:pHead2;
            b= b ? b->next:pHead1;
        }
        return a;
    }
};
  1. 链表中环的入口结点

描述
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
数据范围: 0≤10000,1<=结点值<=10000
要求:空间复杂度O(1),时间复杂度 O(n)
例如,输入{1,2},{3,4,5}时
在这里插入图片描述
可以看到环的入口结点的结点值为3,所以返回结点值为3的结点。

题目抽象:给定一个单链表,如果有环,返回环的入口结点,否则,返回nullptr
方法一:哈希法
遍历单链表的每个结点
如果当前结点地址没有出现在set中,则存入set中
否则,出现在set中,则当前结点就是环的入口结点
整个单链表遍历完,若没出现在set中,则不存在环

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead) {
        unordered_set<ListNode*> st;
        while(pHead){
            if(st.count(pHead)==0){
                st.insert(pHead);
                pHead=pHead->next;
            }
            else
                return pHead;
        }
        return nullptr;
        
    }
};

知识点:
(1)unordered_set::find()函数是C++ STL中的内置函数,用于在容器中搜索元素。调用 unordered_set 的 find() 会返回一个迭代器。这个迭代器指向和参数哈希值匹配的元素,如果没有匹配的元素,会返回这个容器的结束迭代器
(2)unordered_set 容器中只可能有一个匹配元素。如果没有,两个迭代器都是容器的结束迭代器。调用成员函数 count() 会返回容器中参数的出现次数。对于 unordered_set 只可能是 0 或 1。当想要知道容器中总共有多少元素时,可以调用成员函数 size()。如果容器中没有元素,成员函数 empty() 会返回 true。

方法2:快慢指针
通过定义slow和fast指针,slow每走一步,fast走两步,若是有环,则一定会在环的某个结点处相遇(slow == fast),根据分析计算,可知从相遇处到入口结点的距离与头结点与入口结点的距离相同。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead) {
        if(!pHead)
            return nullptr;
        ListNode* fast=pHead;
        ListNode* slow=pHead;
        while(fast && fast->next){
            fast=fast->next->next;
            slow=slow->next;
            if(fast==slow)
                break;
        }
        if(!fast || !fast->next)
            return nullptr;
        fast=pHead;
        while(fast!=slow){
            fast=fast->next;
            slow=slow->next;
        }
        return fast;
        
        
    }
};

详解:
在这里插入图片描述
那么在快慢指针同时到达相遇结点C处时,将快指针fast重新放到头结点A,慢指针slow的位置不变,且快指针的速度改为与慢指针slow相同,那么快指针fast从头结点A走过X路程后到达环的入口结点B,慢指针slow从快慢指针相遇节点C走过x路程后也到达了环的入口结点B,也即他们再次相遇时的节点就是环的入口结点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值