文章目录
链表篇
第一题: 从尾到头打印链表
解题思路:
方法一: 先将输入的值按组存入到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结点。又因为我们我们要指向前一个结点,所以要定义一个指针指向前一个结点。最后我们用三个指针进行操作。
创建 左 中 右 三个指针,中间指针不断指向前一个(左指针),右边指针来判断移动边界!如下图所示:
方法二: 最简单的一种方式就是使用栈,因为栈是先进后出的。
不过方法二也有两种小方法,一是使用栈的类型为int型栈,直接将栈中的元素进行修改;二是使用栈的类型为指针类型,将指针朝向进行修改。
实现原理就是把链表节点一个个入栈,当全部入栈完之后再一个个出栈,出栈的时候在把出栈的结点串成一个新的链表。原理如下
代码部分:
/代码模板
/*
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;
}
};
//方法二-1
//这种思路是直接修改指针内部的val
class Solution {
public:
ListNode* reverseList(ListNode* head) {
stack<int> sta; //这里注意,定义的栈是int型的,不是ListNode*型的
ListNode* p=head;
while(p!=NULL) //借助p将head中的元素全部压入栈中
{
sta.push(p->val);
p=p->next;
}
p=head; //重新将p指向head位置,再借助p去修改head中的值
while(!sta.empty())
{
p->val=sta.top();
sta.pop();
p=p->next;
}
return head;
}
};
//这种方法不用像其他借助栈的方法一样,最后一个指针还要置空
/方法二-2
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;
}
};
第三题: 合并两个排序的链表
解题思路:
方法1:迭代法
两个链表L1、L2,从两个链表的第一个元素开始比较,两者中的较小者传递给新链表,传递完之后,链表指向下一个元素,新链表也要指向下一个空元素,用于接受下一次这两者中较小的比较值。
初始化: 定义cur指向新链表的头结点
小技巧: 一般创建单链表,都会设一个虚拟头结点,也叫哨兵,因为这样每一个结点都有一个前驱结点。具体表现为代码中的第四行。
操作:
- 如果L1指向的结点值小于等于L2指向的结点值,则将l1指向的结点链接到cur的next指针,然后L1指向下一个结点值
- 否则,将l2指向的结点链接到cur的next指针,让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) {
}
};
//方法1
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;
}
};
//方法二
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
if(pHead1 == nullptr || pHead2 == nullptr){ //没有元素的话,数组为空,就不能排序,所以要先判断
return pHead1 == nullptr ? pHead2 : pHead1;
}
vector<int> arr;
while(pHead1 != nullptr){
arr.push_back(pHead1 -> val);
pHead1 = pHead1 -> next;
}
while(pHead2 != nullptr){
arr.push_back(pHead2 -> val);
pHead2 = pHead2 -> next;
}
sort(arr.begin(),arr.end());
ListNode* res = new ListNode(0);
ListNode* p = res;
for(int i = 0; i < arr.size(); i++){
p -> next = new ListNode(arr[i]);
p = p -> next;
}
return res->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;
}
};
第八题: 删除有序链表中重复的元素-I
解题思路:
假设当前链表中有以下元素:1,1,2
定义一个*p=head
判断指针p->next->val和p->val是否相等
如果相等,则将指针p->next=p->next->next,
代码部分:
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
class Solution {
public:
/**
*
* @param head ListNode类
* @return ListNode类
*/
ListNode* deleteDuplicates(ListNode* head) {
if(head==NULL){ //如果链表长度为0或1,直接返回结果
return head;
}
ListNode *cur=head;
while(cur->next){//cur指针不断移动
if(cur->val==cur->next->val){ //如果链表的下下个节点=下个节点
cur->next=cur->next->next; //那么需要将h指向j
}
else{
cur=cur->next;
}
}
return head;
}
};
第九题: 删除链表中的节点(很牛逼)
这道题目乍一看很简单,但是思考了两分钟后我没想出一点头绪,于是去看评论区,好家伙,一语惊醒梦中人!
如何让自己在世界上消失,但又不死? —— 将自己完全变成另一个人,再杀了那个人就行了。
解题思路:
将后面结点的值替换为当前的,然后将当前链表指向下下个结点。
代码部分:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
void deleteNode(ListNode* node) {
node->val=node->next->val;
node->next=node->next->next;
}
};
第十题: 环形链表
解题思路:
解法一:哈希表
从头节点开始,使用哈希表记录下每个节点的地址,如果出现了地址重复的情况,说明这个链表形成了一个环。(set也是哈希表的类型)
代码部分:
class Solution {
public:
bool hasCycle(ListNode *head) {
unordered_set<ListNode*> seen;
while (head != nullptr) {
if (seen.count(head)) { //count()判断元素是否在set容器中。若在:返回1,若不在,返回0。
return true;
}
seen.insert(head); //注意这里插入的是链表的指针,而不是节点中的值,所以即使链表中有重复元素也没关系
head = head->next;
}
return false;
}
};
解法二:双指针
慢指针一次只走一步,快指针一次走两步,如果是一个环的话,那么一定会相遇,即两个指针所指向同一个地址。
代码部分:
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head==nullptr||head->next==nullptr){
return false;
}
ListNode * p1=head;
ListNode * p2=head;
while(p2!=nullptr){
if(p2->next==nullptr){ //不加这行代码的话,走两步可能会出问题,因为p2->next可能为空
return false;
}
p2=p2->next->next; //快指针每次走两步
p1=p1->next;
if(p1==p2){
return true;
}
}
return false;
}
};
第十一题: 判断一个链表是否为回文结构
解题思路:
解法一:栈结构
解题思路:可以先复制当前链表,然后将其中的元素传入到栈中,再将栈中的元素依次弹出和原始链表进行值得比较
优化: 不需要全部比较,因为是回文字符,我么们只需要比较一半长度即可。
class Solution {
public:
bool isPail(ListNode* head) {
stack<int> sta; //这里注意,定义的栈是int型的,不是ListNode*型的
ListNode* p=head;
while(p != nullptr){
sta.push(p->val);
p = p -> next;
}
while(head != nullptr){
int a = sta.top();
sta.pop();
if(head -> val != a){
return false;
}
head = head -> next;
}
return true;
}
};
--优化
class Solution {
public:
bool isPail(ListNode* head) {
stack<int> sta; //这里注意,定义的栈是int型的,不是ListNode*型的
ListNode* p=head;
int len = 0;
while(p != nullptr){
sta.push(p->val);
p = p -> next;
len += 1;
}
len = len /2;
while(len != 0){
int a = sta.top();
sta.pop();
if(head -> val != a){
return false;
}
head = head -> next;
len -= 1;
}
return true;
}
};
解法2:
将链表中的值复制到数组中,然后使用双指针法
class Solution {
public:
bool isPail(ListNode* head) {
vector<int> arr;
ListNode* p = head;
while(p != nullptr){
arr.push_back(p -> val);
p = p -> next;
}
int len = arr.size();
for(int i = 0; i < len/2; i++){
if(arr[i] != arr[len - i - 1]){
return false;
}
}
return true;
}
};
注: 以上题目都是在牛客网的剑指offer题库中刷的,有自己做的,也有不会做看别人精华解题思路,然后总结的。如果侵犯了您的权益,请私聊我。
最后,觉得本文内容对你有所帮助的话,感谢点赞收藏!
导航链接:剑指—队列&栈篇(C++)
导航链接:剑指—算法—动态规划篇(C++)