文章目录
链表篇
第一题: 从尾到头打印链表
解题思路:
方法一: 先将输入的值按组存入到vertor容器中,然后利用reverse函数反转vertor中的数据,最后再返回vertor数组。
方法二: 利用栈先入后出的思想,将输入的元素传入栈中,最后再依次弹出,用一个vector去存储栈结构弹出的元素。
代码部分:
/答题模板
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) :
* val(x), next(NULL) {
* }
* };
*/
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
}
};
/方法一:
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> result;
while(head){
int a=head->val;
result.push_back(a);
head=head->next;
}
reverse(result.begin(), result.end());
return result;
}
};
/方法二
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> vec; //定义一个vector元素用来存放反转后的元素,反转的方式依靠栈结构
stack<int> stk; //从尾到头,类似栈结构的先进后出
ListNode *p = head; //将传入的元素赋给p
if(head)
{
while(p)
{
stk.push(p->val);
p=p->next;
}
while(!stk.empty())
{
vec.push_back(stk.top()); //将栈顶元素添加到vector容器中
stk.pop(); //已经传出的栈顶元素就弹出
}
}
return vec;
}
};
第二题: 反转链表
解题思路:
方法一: 利用三个指针,在原链表上变换 (这个有点复杂,可以看官方解析)
这道题其实就是经过一轮循环,把每个指针的next指向前一个结点。但是我们要考虑到,如果把next指向前一个结点,那么原本的next结点就找不到了,所以我们要在赋值之前保存next结点。又因为我们我们要指向前一个结点,所以要定义一个指针指向前一个结点。最后我们用三个指针进行操作。
创建 左 中 右 三个指针,中间指针不断指向前一个(左指针),右边指针来判断移动边界!如下图所示:
方法二: 最简单的一种方式就是使用栈,因为栈是先进后出的。实现原理就是把链表节点一个个入栈,当全部入栈完之后再一个个出栈,出栈的时候在把出栈的结点串成一个新的链表。原理如下
代码部分:
/代码模板
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
}
};
/方法一
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
ListNode *pre = nullptr;
ListNode *cur = pHead;
ListNode *nex = nullptr; // 这里可以指向nullptr,循环里面要重新指向
while (cur) {
nex = cur->next;
cur->next = pre;
pre = cur;
cur = nex;
}
return pre;
}
};
/方法二
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
if(!pHead || !pHead->next)
return pHead;
stack<ListNode*>stk;
//将链表的结点全部压进栈
while(pHead){
stk.push(pHead);
pHead = pHead->next;
}
ListNode*cur,*nxt;
cur = nxt = stk.top();
stk.pop();
while(!stk.empty()){
nxt ->next = stk.top();
nxt = nxt ->next;
stk.pop();
}
//最后一个结点的next记得要指向nullptr,否则会形成环
nxt ->next =nullptr;
return cur;
}
};
第三题: 合并两个排序的链表
解题思路:
两个链表L1、L2,从两个链表的第一个元素开始比较,两者中的较小者传递给新链表,传递完之后,链表指向下一个元素,新链表也要指向下一个空元素,用于接受下一次这两者中较小的比较值。
初始化: 定义cur指向新链表的头结点
小技巧: 一般创建单链表,都会设一个虚拟头结点,也叫哨兵,因为这样每一个结点都有一个前驱结点。具体表现为代码中的第四行。
操作:
- 如果L1指向的结点值小于等于L2指向的结点值,则将l1指向的结点值链接到cur的next指针,然后L1指向下一个结点值
- 否则,让L2指向下一个结点值
- 循环上面两个步骤,直到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) {
}
};
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;
}
};
第四题: 两个链表的第一个公共结点
解题思路:
使用两个指针N1,N2,一个从链表1的头节点开始遍历,我们记为N1,一个从链表2的头节点开始遍历,我们记为N2。
让N1和N2一起遍历,当N1先走完链表1的尽头(为null)的时候,则从链表2的头节点继续遍历,同样,如果N2先走完了链表2的尽头,则从链表1的头节点继续遍历,也就是说,N1和N2都会遍历链表1和链表2。
因为两个指针,同样的速度,走完同样长度(链表1+链表2),不管两条链表有无相同节点,都能够到达同时到达终点。
(N1最后肯定能到达链表2的终点,N2肯定能到达链表1的终点)。
所以,如何得到公共节点:
有公共节点的时候,N1和N2必会相遇,因为长度一样嘛,速度也一定,必会走到相同的地方的,所以当两者相等的时候,则会第一个公共的节点
无公共节点的时候,此时N1和N2则都会走到终点,那么他们此时都是null,所以也算是相等了。
下面看个动态图,可以更形象的表示这个过程~
代码部分:
/答题模板
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
}
};
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
ListNode *l1 = pHead1, *l2 = pHead2;
while(L1 != L2){
L1 = (NULL==L1)?pHead2:L1->next;
L2 = (NULL==L2)?pHead1:L2->next;
}
return L1;
}
};
代码解读:
第4行:定义了两个链表头指针
第五行:如果l1不等于l2,说明两者指向不同,继续执行循环体中的语句,因为在解题思路中说过了,不管两条链表有无相同节点,都能够到达同时到达终点,所以这个while循环在满足条件后一定会退出,如果推出后返回的是{},说明两个链表没有公共节点,否则返回的就是两者的公共节点。
第六行和第七行:如果当前指针的内容为空,那么指针指向对方的头节点,否则继续指向自身链表的下一个节点
第五题: 链表中环的入口节点
解题思路:
方法一: 利用哈希表
- 遍历单链表的每个结点
- 如果当前结点地址没有出现在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) {
}
};
/方法一
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
unordered_set<ListNode*> st; //定义一个无序集合,用来记录出现的结点
while(pHead){
//如果pHead为空,说明是单链表
if(st.find(pHead)==st.end()){
//查看刚插入的数是否存在于集合中
st.insert(pHead); //将当前pHead指向的元素插入到集合中
pHead=pHead->next;
}
else{
return pHead;
}
}
return nullptr;
}
};
第六题: 链表中倒数最后k个结点
解题思路:
方法一: 数组法,将输入的集合中的数的每个地址用一个数组保存起来,然后根据k值,直接返回数组中第k个及之后的数。
方法二: 双指针法,该方法来自牛客网其中的一个解题思路。
代码部分:
/代码模板
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) : val(x), next(nullptr) {}
* };
*/
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
这里要返回的类型是ListNode*
ListNode* FindKthToTail(ListNode* pHead, int k) {
// write code here
}
};
/方法一
class Solution {
public:
ListNode* FindKthToTail(ListNode* pHead, int k) {
vector<ListNode*> res; //定义一个数组,里面的变量是指针类型
for (; pHead; pHead = pHead->next)
res.emplace_back(pHead); // 把每个节点指针放入数组,
if (k > res.size() || !k) return nullptr; // 判断k值是否合法
return res[res.size() - k]; //返回
}
};
解释1:
for循环语句括号中的各表达式可以省略,但表达式之间的
间隔符 ( 分号 )不能缺省
解释2:
在 C++11 之后,vector 容器中添加了新的方法:emplace_back() ,
和 push_back() 一样的是都是在容器末尾添加一个新的元素进去,不同的是
emplace_back() 在效率上相比较于 push_back() 有了一定的提升。
过程:
输入:{
1,2,3,4,5},2
一开始的时候:pHead指向1的地址,此时执行res.emplace_back(pHead);
res[0]=[1,2,3,4,5];
pHead=pHead->next;
此时pHead指向2的地址,继续执行res.emplace_back(pHead);
res[1]=[2,3,4,5];
res[2]=[3,4,5];
res[3]=[4,5];
res[4]=[5];
最后返回的是return res[res.size() - k];
因为res.size()=5,故返回的是res[3],正好是4和5
/方法二
class Solution {
public:
ListNode* FindKthToTail(ListNode* pHead, int k) {
ListNode* r = pHead;
while (k-- && r){
r = r->next; // 移动右侧指针造成 k 的距离差
}
if (k >= 0) return nullptr; // 此时说明是因为r指向的是NULL,while才退出的,即k比链表长度长
ListNode* l = pHead;
while (r){
r = r->next, l = l->next; // 两个指针一起移动找到倒数第 k 个节点
}
return l;
}
};
第七题: 删除链表的节点
解题思路:
首先,如果要删除的是头节点,那么我们只需把头节点head指向下一个节点即可;如果要删除的节点是在链表内的,那么可以用下图的思想去进行删除。
在上图中,假设我们要删除的是i节点,那么我们只需要把h节点的指向从i改成j。
代码部分:
class Solution {
public
ListNode* deleteNode(ListNode* head, int val) {
if(head->val==val){
//如果头节点就是要删除的节点,那么直接把头节点向后移
return head->next;
}
ListNode *cur=head;
ListNode *res=cur;//指向cur的初始位置
while(cur->next){
//cur指针不断移动
if(cur->next->val==val){
//这里可以看成是h->next=i中的值
ListNode *temp=cur->next->next; //那么需要将h指向j
cur->next=temp;
}
cur=cur->next;
}
return res;
}
};
注: 以上题目都是在牛客网的剑指offer题库中刷的,有自己做的,也有不会做看别人精华解题思路,然后总结的。如果侵犯了您的权益,请私聊我。
最后,觉得本文内容对你有所帮助的话,感谢点赞收藏!
导航链接:剑指—队列&栈篇(C++)
导航链接:剑指—算法—动态规划篇(C++)