剑指offer第2版:链表系列

剑指offer第2版:链表系列

一、p58-JZ6 从尾到头打印链表(递归/栈)

从尾到头打印链表_牛客题霸_牛客网

 解法1、递归,每访问一个节点时,先递归输出它后面的节点,再输出该节点自身,但是这样的话可能导致函数的调用层级很深,从而导致函数调用栈溢出。

class Solution { public: void print(vector<int>&ret,ListNode* head){ if(head==nullptr) return; print(ret,head->next); ret.emplace_back(head->val); } vector<int> printListFromTailToHead(ListNode* head) { vector<int> ret; print(ret,head); return ret; } }; 

解法2:利用栈后进先出的特性,可以避免栈溢出的情况,但是会消耗一部分内存

class Solution { public: vector<int> printListFromTailToHead(ListNode* head) { if(!head) return {}; stack<int> st; vector<int> ret; while(head){//先都入栈 st.push(head->val); head=head->next; } //然后出栈放到数组里面 while(!st.empty()){ ret.emplace_back(st.top()); st.pop(); } return ret; } };

解法3,因为该题返回数组,所以直接保存在数组里然后直接逆序即可!

class Solution { public: vector<int> printListFromTailToHead(ListNode* head) { if(!head) return {}; vector<int> ret; while(head) { ret.emplace_back(head->val); head=head->next; } reverse(ret.begin(),ret.end()); return ret; } };

二、p119-JZ18 删除链表的节点

删除链表的节点_牛客题霸_牛客网

因为我们需要找到前面的节点链接后面的节点,所以我们必须停留在该节点的前一个节点 

class Solution { public: ListNode* deleteNode(ListNode* head, int val) { if(!head) return nullptr; if(head->val==val) return head->next; //此时说明节点在中间 此时我们要记住前驱节点和后继节点(] ListNode* cur=head; while(cur->next&&cur->next->val!=val) cur=cur->next; //走到这说明找到了 cur的下一个位置就是要删除的 此时就把他丢掉即可 cur->next=cur->next->next; return head; } };

三、扩展p122-JZ18 删除排序链表的重复节点(前后指针)

删除链表中重复的结点_牛客题霸_牛客网

我们要用两个指针,左开右闭(]   这里要考虑特别多的特殊情况,如:全部相同,全部不相同,部分相同等,为了方便解题我们定义哨兵头结点,主要是应对全部相同的情况

class Solution { public: ListNode* deleteDuplication(ListNode* pHead) { if(pHead==nullptr) return nullptr; //prev和rear 前开后闭(] //定义一个虚拟头节点 ListNode*newhead=new ListNode(0);//哨兵节点 是为了防止讨论整张表都是重复的情况 newhead->next=pHead; ListNode* prev=newhead; //prev始终在前面 ListNode* rear=pHead; while(rear!=nullptr){//一直处理到尾巴 //如果没有重复 就两个一起往后走 while(rear->next&&rear->val!=rear->next->val){//为了保证前开后闭 所以要比next prev=prev->next; rear=rear->next; } //如果发现重复了 就让rear走 while(rear->next&&rear->val==rear->next->val) rear=rear->next; //此时有3种情况 前两种可以一起搞 //1、(prev.rear] 正常在中间 //2、(prev.rear] 但此时rear->next==nullptr 此时后面要全部去掉 //3、没有重复 这种情况prev->next==rear 就可以不用处理了 不管 if(prev->next!=rear) prev->next=rear->next;//此时是前两种情况 就直接开始删 rear=rear->next; //无论是第一种还是第二种 都要让rear指向下一个 } return newhead->next; } };

 四、p134-JZ22 链表中倒数第k个结点(前后指针)

链表中倒数最后k个结点_牛客题霸_牛客网

       可以遍历两次链表,第一次链表算长度,然后根据长度算出倒数第k个是第几个,然后第二次再去找,但是还有更好的方法只需要遍历一次链表即可。 

       可以定义两个指针,一个指针先走k步,再让另一个指针跟在后面,使用“前后指针”的方式,当前面的指针到达结尾, 后面的指针,也就是倒数第k个节点 (但是要注意k超过节点数的情况)

class Solution { public: //倒数第k个节点 用快慢指针 当快指针先走完的时候 此时慢指针就是倒数k ListNode* FindKthToTail(ListNode* head, int k) { if(!head||k==0) return nullptr; ListNode*fast,*slow; fast=slow=head;//约定头结点是第一个节点 while(k>0&&fast){ fast=fast->next; --k; } while(fast){ fast=fast->next; slow=slow->next; } //此时有两种情况 要么k>0说明不存在 k==0说明可以找到 return k>0?nullptr:slow; } };

注意k是无符号整数,如果是0的话,一定不能让他-1 

 五、p142-JZ24 反转链表(三指针/头插)

反转链表_牛客题霸_牛客网

1、定义三个指针,整体右移,边移动,边翻转,保证不会断链。

class Solution { public: //1、三指针翻转法 2、头插法 ListNode* ReverseList(ListNode* head) { if(!head||!head->next) return head;//此时就没必要翻转了 //能走到这至少是有两个节点的 ListNode*p1=head; ListNode*p2=head->next; ListNode*p3=head->next->next; while(p3){//p3为空的时候就不进去了 p2->next=p1; p1=p2; p2=p3; p3=p3->next; } p2->next=p1; head->next=nullptr;//头指针要让他为空!!! return p2; } };

2、可以采用头插思想进行翻转

class Solution { public: ListNode* ReverseList(ListNode* head) { if(!head||!head->next) return head;//此时就没必要翻转了 //能走到这至少是有两个节点的 ListNode*newhead=nullptr;//以这个为头 while(head){ ListNode* t=head; head=head->next; t->next=newhead; newhead=t; } return newhead; } };

六、p145-JZ25 合并两个排序的链表(双指针/递归)

合并两个排序的链表_牛客题霸_牛客网

思路1:可以一个一个节点的归并,可以搞一个哨兵节点。

class Solution { public: ListNode* Merge(ListNode* pHead1, ListNode* pHead2) { if(!pHead1) return pHead2; if(!pHead2) return pHead1; //此时两个都不为空链表了 //设计一个哨兵节点 然后不断尾插 ListNode*newhead=new ListNode(-1); ListNode*rear=newhead;//用来方便尾插的 while(pHead1&&pHead2){ //当两个都不为空的时候 if(pHead1->val<pHead2->val){ rear->next=pHead1;//尾插 pHead1=pHead1->next;//迭代 } else{ rear->next=pHead2;//尾插 pHead2=pHead2->next;//迭代 } rear=rear->next;//继续往下一个位置遍历 } // 此时肯定其中一方走完了 if(!pHead1) rear->next=pHead2; if(!pHead2) rear->next=pHead1; return newhead->next; } };

思路2:可以递归完成

class Solution { public: ListNode* Merge(ListNode* pHead1, ListNode* pHead2) { if (!pHead1) return pHead2; if (!pHead2) return pHead1; if (pHead1->val < pHead2->val) { pHead1->next = Merge(pHead1->next, pHead2); return pHead1; } else { pHead2->next = Merge(pHead1, pHead2->next); return pHead2; } } };

七、p253-JZ52 两个链表的第一个公共节点(双指针/栈/set)

两个链表的第一个公共结点_牛客题霸_牛客网

         题目要求是单链表,每个节点只有一个next指针,因此从第一个公共节点开始,之后他们所有的节点都是重合的,不可能再出现分叉!

        我们会发现如果两个链表有公共节点,那么至少链表的尾部一定是公共节点,那么如果能够从尾部开始走那么最后一个相同的节点就是第一个公共节点,但是因为单链表不可能从尾部开始走,所以这里我们就必须借助栈结构“后进先出”的特性!!

方法1:借助两个辅助栈,然后将节点全部丢到栈里面,然后从栈顶开始遍历直到找到最后一个相同的节点。

class Solution { public: ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) { if (!pHead1 || !pHead2) return nullptr; if(pHead1==pHead2) return pHead1;//防止第一个就是 //设计两个辅助栈 stack<ListNode*> st1; stack<ListNode*> st2; while(pHead1){ st1.push(pHead1); pHead1=pHead1->next; } while (pHead2) { st2.push(pHead2); pHead2=pHead2->next; } if(st1.top()!=st2.top()) return nullptr; while(!st1.empty()&&!st2.empty()){ ListNode* t1=st1.top(),*t2=st2.top(); st1.pop(); st2.pop(); if(st1.empty()||st2.empty()||st1.top()!=st2.top()) return t1; //判空是为了防止其中一个链表的头结点是另一个链表的子节点的情况 } return nullptr; } };

方法2:本质是让长的链表先走abs(length1-length2)步,后面大家的步调一致,往后找第一个地址相同的 节点,就是题目要求的节点,所以需要各自遍历两次链表。 

class Solution { public: ListNode* getlenth(ListNode* cur, int& count) { while (cur->next) { ++count; cur = cur->next; } return cur; } ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) { if (!pHead1 || !pHead2) return nullptr; //分开数两个链表 int count1 = 0, count2 = 0; //封装一个函数去算两个链表的长度 //如果链表走到最后都不是一个节点,说明说明一定没有公共节点 ListNode* end1 = getlenth(pHead1, count1); ListNode* end2 = getlenth(pHead2, count2); if (end1 != end2) return nullptr; //走到这说明一定有公共节点 这个时候要让长一点的链表先走上差值步 if (count1 > count2) //如果第一个比较长 while (count1 - count2) { --count1; pHead1 = pHead1->next; } else while (count2 - count1) { --count2; pHead2 = pHead2->next; } //此时两个一起走 while (pHead1 != pHead2) { pHead1 = pHead1->next; pHead2 = pHead2->next; } return pHead1; } };

 方法3:直接把其中一个链表节点都保存到set里面,然后遍历另一个链表,只要出现了就返回

class Solution { public: ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) { if (!pHead1 || !pHead2) return nullptr; if(pHead1==pHead2) return pHead1; unordered_set<ListNode*> s; while(pHead1){ s.insert(pHead1); pHead1=pHead1->next; } while(pHead2){ if(s.count(pHead2)) return pHead2; pHead2=pHead2->next; } return nullptr; } }; 

八、p187-JZ35 复杂链表的复制(模拟/哈希)

复杂链表的复制_牛客题霸_牛客网

该题的难点就在于如果直接拷贝,那么ramdom指针要拷贝的话还需要去原链表里面一次次遍历长度才可以,此时的时间复杂度是n^2   因此我们在创建节点的时候,将新的节点连接在原链表后面,然后修改ramdom指针之后,再把链表拆开!!

方法1:模拟,让新表接在原表的后面,然后修改random指针之后,再把表拆下来返回

/* struct RandomListNode { int label; struct RandomListNode *next, *random; RandomListNode(int x) : label(x), next(NULL), random(NULL) { } }; */ class Solution { public: RandomListNode* Clone(RandomListNode* pHead) { //如果直接拷贝,那么ramdom指针要拷贝的话还需要去原链表里面一次次遍历长度才可以,此时的时间复杂度是n^2 因此我们在创建节点的时候,将新的节点连接在后,然后修改ramdom指针之后,再把链表拆开!! if(!pHead) return nullptr; //走到这 至少是有一个节点的! RandomListNode* cur=pHead;//帮助我们进行遍历 while(cur){ RandomListNode*newnode =new RandomListNode(cur->label);//创建新节点 //将新节点接到cur后面 newnode->next=cur->next; cur->next=newnode; cur=cur->next->next;//迭代 } //此时节点都接上了,然后我们要去修改他们的ramdom指针 cur=pHead; while(cur){ if(cur->random) cur->next->random=cur->random->next; cur=cur->next->next;//迭代 } //然后此时可以拆了 RandomListNode*newhead=new RandomListNode(-1);//哨兵头节点 RandomListNode*newtail=newhead; cur=pHead; while(cur){ RandomListNode*temp=cur->next;//把这个点留住 //cur向后接 cur->next=cur->next->next; //该节点接在新表后面 newtail->next=temp; newtail=newtail->next; cur=cur->next;//向后迭代 } return newhead->next; } };

上述问题主要是为了搜索ramdom指针指向节点而产生的效率问题,所以我们把他接在原链表的节点后面是为了方便通过原链表快速修改ramdom指针,那么如果我们可以直接让新节点和原节点建立联系呢??比如说通过哈希表建立映射关系!!

方法2:哈希,第一次遍历创建新链表和原链表每个节点的映射关系,第二遍去改ramdom指针(空间换时间)

#include <unordered_map> class Solution { public: RandomListNode* Clone(RandomListNode* pHead) { if(!pHead) return nullptr; //走到这 至少是有一个节点的! unordered_map<RandomListNode*,RandomListNode*> hash; RandomListNode*newhead,*newtail; newtail=newhead=nullptr;//一开始为空 RandomListNode* cur=pHead;//帮助我们进行遍历 while(cur){ RandomListNode*newnode =new RandomListNode(cur->label);//创建新节点 hash[cur]=newnode;//建立起映射关系 if(!newtail) newtail=newhead=newnode; else{//将新节点接到新表上 newtail->next=newnode; newtail=newtail->next; } cur=cur->next;//迭代 } //第二次遍历去改新链表的ramdom指针 cur=pHead; RandomListNode* copy=newhead; while(cur){ if(cur->random) copy->random=hash[cur->random];//不为空 就改一下ramdom //然后向后迭代 cur=cur->next; copy=copy->next; } return newhead; } };

九、扩展p139JZ23 链表判环(快慢指针)

判断链表中是否有环_牛客题霸_牛客网

 如果链表有环的话,那么用快慢指针,fast会逐步接近追上slow,如果无环那么fast最后会走向空

class Solution { public: bool hasCycle(ListNode *head) { if(!head||!head->next) return false; //走到这 至少有两个节点 //快慢指针追击 ListNode*fast=head; ListNode*slow=head; while(fast&&fast->next){ fast=fast->next->next; slow=slow->next; if(fast==slow) return true; } return false; } };

十、p139-JZ23 链表中环的入口节点(快慢指针+环性质)

链表中环的入口结点_牛客题霸_牛客网

解法1:利用相遇点到入口点的距离=链表头到入口点的距离  这个性质

class Solution { public: ListNode* EntryNodeOfLoop(ListNode* pHead) { if(!pHead||!pHead->next) 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!=slow) return nullptr;//不相等说明没有环 while(pHead!=fast){ pHead=pHead->next; fast=fast->next; } return pHead; } };

解法2:在相遇点将链表拆开,转化成两个链表的找第一个相交节点的问题

class Solution { public: ListNode* getlenth(ListNode* cur, int& count) { while (cur->next) { ++count; cur = cur->next; } return cur; } ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) { if (!pHead1 || !pHead2) return nullptr; //分开数两个链表 int count1 = 0, count2 = 0; //封装一个函数去算两个链表的长度 //如果链表走到最后都不是一个节点,说明说明一定没有公共节点 ListNode* end1 = getlenth(pHead1, count1); ListNode* end2 = getlenth(pHead2, count2); if (end1 != end2) return nullptr; //走到这说明一定有公共节点 这个时候要让长一点的链表先走上差值步 if (count1 > count2) //如果第一个比较长 while (count1 - count2) { --count1; pHead1 = pHead1->next; } else while (count2 - count1) { --count2; pHead2 = pHead2->next; } //此时两个一起走 while (pHead1 != pHead2) { pHead1 = pHead1->next; pHead2 = pHead2->next; } return pHead1; } ListNode* EntryNodeOfLoop(ListNode* pHead) { if(!pHead||!pHead->next) 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!=slow) return nullptr;//不相等说明没有环 //要把环给断掉 fast=fast->next; slow->next=nullptr; return FindFirstCommonNode(pHead,fast); } };

Read more

Flutter 三方库 wallet_connect 的鸿蒙化适配指南 - 实现 Web3 钱包协议连接、支持 DApp 授权登录与跨链交易签名实战

Flutter 三方库 wallet_connect 的鸿蒙化适配指南 - 实现 Web3 钱包协议连接、支持 DApp 授权登录与跨链交易签名实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 wallet_connect 的鸿蒙化适配指南 - 实现 Web3 钱包协议连接、支持 DApp 授权登录与跨链交易签名实战 前言 在进行 Flutter for OpenHarmony 的去中心化应用(DApp)或加密货币钱包开发时,支持标准的 WalletConnect 协议是链接用户钱包的关键。wallet_connect 是该协议的 Dart 实现,它能让你的鸿蒙 App 安全地与 MetaMask、Trust Wallet 等钱包建立双向加密连接。本文将探讨如何在鸿蒙系统下构建安全、稳定的 Web3 授权流程。 一、原理解析 / 概念介绍 1.1 基础原理

By Ne0inhk

Qwen3-VL-2B部署案例:博物馆导览机器人系统

Qwen3-VL-2B部署案例:博物馆导览机器人系统 1. 引言:视觉语言模型在智能导览中的应用价值 随着人工智能技术的发展,视觉语言模型(Vision-Language Model, VLM)正逐步从实验室走向实际应用场景。在公共服务领域,尤其是博物馆、美术馆等文化场所,智能化导览系统的需求日益增长。传统的语音讲解或静态图文介绍已难以满足用户对交互性、个性化和沉浸式体验的期待。 Qwen3-VL-2B-Instruct 作为阿里云开源的最新一代视觉语言模型,具备强大的图文理解、空间感知与多模态推理能力,为构建高可用的导览机器人系统提供了理想的技术底座。该模型支持图像识别、OCR解析、语义问答、上下文记忆等多种功能,并内置针对指令任务优化的 Instruct 版本,能够快速适配定制化场景。 本文将围绕 Qwen3-VL-2B-Instruct 模型,结合 Qwen3-VL-WEBUI 部署方案,详细介绍其在博物馆导览机器人系统中的落地实践,涵盖环境搭建、功能实现、关键代码及性能优化建议。 2. 技术选型与系统架构设计 2.1 为什么选择 Qwen3-VL-2B-Inst

By Ne0inhk
【PX4+ROS完全指南】从零实现无人机Offboard控制:模式解析与实战

【PX4+ROS完全指南】从零实现无人机Offboard控制:模式解析与实战

引言 无人机自主飞行是机器人领域的热门方向,而PX4作为功能强大的开源飞控,配合ROS(机器人操作系统)的灵活性与生态,成为实现高级自主飞行的黄金组合。然而,许多初学者对PX4的飞行模式理解不清,更不知道如何通过ROS编写可靠的Offboard控制程序。 本文将带你彻底搞懂PX4 6大核心飞行模式,实现无人机的自动起飞、悬停、轨迹跟踪(圆形/方形/螺旋)与降落。 亮点一览: * ✅ 深度解析PX4飞行模式(稳定/定高/位置/自动/Offboard) * ✅ 明确ROS可控制的模式与指令接口 * ✅ 完整的ROS功能包(C++实现,状态机设计) * ✅ 支持位置控制与速度控制双模式 * ✅ 内置圆形、方形、螺旋轨迹生成器 * ✅ 详细的安全机制与失效保护配置 无论你是准备参加比赛、做科研,还是想入门无人机开发,这篇文章都将是你宝贵的参考资料。 第一部分:PX4飞行模式深度剖析 PX4的飞行模式可以看作一个控制权逐级递增的层级结构。理解这些模式是编写控制程序的前提。 1. 稳定模式(STABILIZED / MANUAL / ACRO) * 核心特点:

By Ne0inhk