注:
题号JZ×× :代表牛客网上的题号
面试题×× :代表牛客上没有,《剑指offer》上的题号
文章目录
面试题6. 从尾到头打印链表
- 递归或使用栈
JZ14链表中倒数第k个结点
题目描述
输入一个链表,输出该链表中倒数第k个结点。
示例1
输入
1,{
1,2,3,4,5}
返回值
{
5}
注:
1、链表结构含有头结点
。
2、参考这里
方法一:普通解法
题解:求倒数第k个,可以转换成求正数第多少个
。
特例:
1、k<=0 或者头结点最开始就为空,也就是没有节点
2、节点的总个数 < K
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if (!pListHead || k <= 0) return nullptr;
int n = 0;
ListNode *cur = pListHead;
while (cur) {
cur = cur->next;
++n;
}
if (n < k) return nullptr;
n -= k;
while (n--) {
//或for(int i=1;i<=n;++i)
pListHead = pListHead->next;
}
return pListHead;
}
};
时间复杂度:O(2*n),n为链表的总长度,如果k总是在倒数第一个节点,那么此方法需要遍历链表2次,一次确定链表长度n,一次从头节点找到第k个节点。
空间复杂度:O(1)
方法二:严格的O(n)解法,快慢指针
题解:
从图中可以看出,倒数第k个节点与最后的空节点之间有k个指针,于是我们可以想到可以通过平移
来到达最后的状态。
注:
这里的平移是通过快慢指针(双指针)实现的。
如上图:
1、快、慢指针都指向头结点,
1、首先让快指针先行k步,
2、接着让快慢指针每次同行一步(保持快慢指针中间始终有k个指针),直到快指针指向空节点,慢指针就是倒数第K个节点。
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if(!pListHead||k<=0) return nullptr;
ListNode* fastPtr=pListHead;
ListNode* slowPtr=pListHead;
while(k--) {
if(fastPtr) fastPtr=fastPtr->next;
else return nullptr;//如果单链表长度 < K,直接返回
}
while(fastPtr){
fastPtr=fastPtr->next;
slowPtr=slowPtr->next;
}
return slowPtr;
}
};
时间复杂度:O(n),不管如何,都只遍历一次单链表
空间复杂度:O(1)
面试题18删除链表的结点
1、需要知道要删除节点的前一个结点(需要从头节点顺序查找遍历)
如下图(b)
ListNode* ppHead= new ListNode(-1);//new一个新结点作为指向(point)头结点(pHead)的节点(ppHead)
ppHead->next=pHead;
ListNode* pre=ppHead;
while(pre->next!=pToBeDeleted){
pre=pre->next;}
pre->next=pToBeDeleted->next;
return ppHead->next
2、不需知道要删除节点的前一个结点。
知道被删除结点,很容易得到下一个结点,我们可以把下一个结点的内容复制到需要删除的结点上,覆盖原有的内容,再把下一个结点删除,再把本来要删除的结点指向下一个结点的下一个结点,这样就相当于删除了指定结点。具体流程如图(c)
注:
注:
JZ56删除链表中重复的结点 (运用到面试题18删除结点的知识)
题目描述
在一个排序的
链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留
,返回链表头指针
。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
示例1
输入
{
1,2,3,3,4,4,5}
返回值
{
1,2,5}
题解:参考这里
方法一:使用set,暴力解法
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead)
{
if (!pHead) return pHead;
set<int> st;
ListNode *pre = pHead;
ListNode *cur = pHead->next;
while (cur) {
if (pre->val == cur->val) {
st.insert(pre->val);
}
pre = pre->next;
cur = cur->next;
}
ListNode *vhead = new ListNode(-1);
vhead->next = pHead;
pre = vhead;
cur = pHead;
while (cur) {
if (st.count(cur->val)) {
cur = cur->next;
pre->next = cur;
}
else {
pre = pre->next;
cur = cur->next;
}
}
return vhead->next;
}
};
时间复杂度:O(2n),遍历了2次单链表
空间复杂度:最坏O(n), 最好O(1)
方法二:直接删除法(推荐)
在遍历单链表的时候,检查当前节点与下一点是否为相同值,如果相同,继续查找相同值的最大长度,然后指针改变指向。
Java
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public ListNode deleteDuplication(ListNode pHead) {
ListNode ppHead = new ListNode(0);
ppHead.next = pHead;
ListNode pre = ppHead;
ListNode cur = pHead;
while(cur != null){
if(cur.next != null && cur.val == cur.next.val){
while(cur.next != null && cur.val == cur.next.val){
cur = cur.next;
}
pre.next = cur.next;
cur = cur.next;
}
else{
pre = cur;
cur = cur.next;
}
}
return ppHead.next;
}
}
C++
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead)
{
//指向头结点的指针(头指针),用于删除头结点;并且题目要求返回链表“头指针”,所以
ListNode* ppHead= new ListNode(-1);//new一个新结点作为指向(point)头结点(pHead)的节点(ppHead)
ppHead->next=pHead;
ListNode* pre=ppHead;
ListNode* cur=pHead;
while(cur){
//链表是否为空,空链接就不需要处理
//cur->next为假,说明链表只有一个结点,不存在结点重复问题,直接返回。
if(cur->next&&cur->val==cur->next->val){
//检查当前节点与下一点是否为相同值
cur=cur->next;
while(cur->next&&cur->val==cur->next->val){
//如果相同,继续查找相同值的最大长度,然后指针改变指向。
cur=cur->next;
}
cur=cur->next;
pre->next=cur;
}
else{
pre=cur;
cur=cur->next;
}
}
return ppHead->next;//之所以不返回pHead是因为pHead指向的是原来链表的头结点,而ppHead->next是指向去除重复结点后链表的头结点。
//当头结点是重复结点的时候,是有区别的
}
};
时间复杂度:O(n)
空间复杂度:O(1)
JZ55链表中环的入口结点
题目描述
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
例如图3.8环的入口节点为结点3。
方法一:哈希法
1、遍历单链表的每个结点
2、如果当前结点地址没有出现在set中,则存入set中
3、否则,出现在set中,则当前结点就是环的入口结点
4、整个单链表遍历完,若没出现在set中,则不存在环
代码:
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
set<ListNode*> st;
while(pHead){
if(st.find(pHead)==st.end()){
st.insert(pHead);
pHead=pHead->next;
}
else{
return pHead;
}
}
return nullptr;
}
};
时间复杂度:O(n)
空间复杂度:O(n),最坏情况下,单链表的所有结点都在存入set
方法二:双指针(快慢指针)
题解:
1、
2、
3、
Java
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
ListNode meet = meetListNode(pHead);
if(meet == null) return null;
int numOfLoop = 1;
ListNode temp = meet.next;
while(temp != meet){
++numOfLoop;
temp = temp.next;
}
ListNode slow = pHead;
ListNode fast = pHead;
while(--numOfLoop != 0){
fast = fast.next;
}
while(fast.next != slow){
slow = slow.next;
fast = fast.next;
}
return slow;
}
ListNode meetListNode(ListNode pHead){
ListNode slow = pHead;
ListNode fast = pHead;
while(slow!=null && fast!=null){
slow = slow.next;
if(slow == null) return null;
fast = fast.next;
if(fast == null) return null;
fast = fast.next;
if(slow == fast) return fast;
}
return null;
}
}
C++
代码:
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
ListNode* meetingNode = MeetingNode(pHead);
if(meetingNode==nullptr) return nullptr;
int numNodeInLoop=1;
ListNode* temNode=meetingNode->next;
while(temNode!=meetingNode){
++numNodeInLoop;
temNode=temNode->next;
}
ListNode* p1=pHead;
for(int i=1;i<=numNodeInLoop;i++){
p1=p1->next;
}
ListNode* p2=pHead;
while(p1!=p2){
p1=p1->next;
p2=p2->next;
}
return p2;
}
ListNode* MeetingNode(ListNode* pHead){
if(pHead==nullptr)
return nullptr;
ListNode* slow=pHead;
ListNode* fast=pHead;
while(slow && fast ){
slow=slow->next;
if(slow==nullptr) return nullptr;
fast=fast->next;
if(fast==nullptr) return nullptr;
fast=fast->next;
if(slow==fast)
return fast;
}
return nullptr;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
JZ15反转链表
题目
输入一个链表,反转链表后,输出新链表的表头。
示例1
输入
{
1,2,3}
返回值
{
3,2,1}
参考这里
方法一:构造链表(不推荐)
方法二:指针解法(推荐)
此题想考察的是:如何调整链表指针,来达到反转链表的目的。所以用方法二
三指针解法
1、cur :指向待反转链表的第一个节点,最开始第一个节点待反转,所以初始化cur=head
2、pre:反转操作是让cur->next指向cur前面一个节点,所以还需要pre指向正在反转结点的前一个结点(即,已经反转好的链表的最后一个节点),反转第一个结点时,无前一个结点,所以初始化pre=nullptr
3、next: 进行反转操作cur->next后,由于cur->next改变指向,所以原来cur后的结点找不到了,所以还需要结点next将正在反转结点的后一个结点保存下来,初始化next=cur->next
接下来,循环执行以下三个操作
1)nex = cur->next, 保存作用
2)cur->next = pre 未反转链表的第一个节点的下个指针指向已反转链表的最后一个节点
3)pre = cur, cur = nex; 指针后移,操作下一个未反转链表的第一个节点
循环条件,当然是cur != nullptr
循环结束后,cur当然为nullptr,所以返回pre,即为反转后的头结点
代码:
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
if(pHead==nullptr)
return nullptr;
ListNode* cur=pHead;
ListNode* pre=nullptr;
ListNode* next=cur->next;
while(cur){
next=cur->next;
cur->next=pre;
pre=cur;
cur=next;
}
return pre;
}
};
时间复杂度:O(n), 遍历一次链表
空间复杂度:O(1)
JZ16合并两个排序的链表
题目描述
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
示例1
输入
{
1,3,5},{
2,4,6}
返回值
{
1,2,3,4,5,6}
方法一:迭代版本求解
初始化:定义cur指向新链表的头结点
操作:
如果l1指向的结点值小于等于l2指向的结点值,则将l1指向的结点值链接到cur的next指针,
然后移动l1的头指针,指向下一个结点值
否则,让l2指向下一个结点值
循环步骤1,2,直到l1或者l2为nullptr
将l1或者l2剩下的部分链接到cur的后面
技巧
一般创建单链表,都会设一个虚拟头结点,也叫哨兵,因为这样每一个结点都有一个前驱结点。
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;
}
};
时间复杂度:O(m+n),m,n分别为两个单链表的长度
空间复杂度:O(1)
方法二:递归版本
模式识别:
首先比较两个链表的头结点,哪个小,哪个作为新链表的头结点;然后继续合并两个链表中剩余的结点。可以将剩余的部分看成两个新链表,接着比较头结点,将小的结点链接在上一步得出的结点上,显然这是一个递归问题(如下图)
1、递归函数功能(原问题):合并两个单链表,返回两个单链表头结点值小的那个节点。
2、递归终止条件:
L1为空,返回L2
L2为空,返回L1
L1,L2都为空,返回nullptr
3、下一步递归(子问题):
如果PHead1的值大于等于pHead2的结点值,那么phead2后续节点和pHead1节点继续递归;
或如果PHead1的值小于pHead2的结点值,那么phead1后续节点和pHead2节点继续递归
代码:
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if(pHead1==nullptr) return pHead2;
if(pHead2==nullptr) return pHead1;
if(pHead1==nullptr&&pHead2==nullptr) return nullptr;
ListNode* pNewHead=nullptr;
if(pHead1->val >= pHead2->val){
pNewHead=pHead2;
pNewHead->next=Merge(pHead1, pHead2->next);
}
else{
pNewHead=pHead1;
pNewHead->next=Merge(pHead1->next, pHead2);
}
return pNewHead;
}
};
时间复杂度:O(m+n)
空间复杂度:O(m+n),每一次递归,递归栈都会保存一个变量,最差情况会保存(m+n)个变量
JZ25复杂链表的复制
题目:
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
注意:
深拷贝(Deep Copy),类似我们常用的“复制粘贴”,与此对应的还有 浅拷贝,它们的区别是
:
浅拷贝
只复制指向某个对象的指针
,而不复制对象本身,新旧对象还是共享同一块内存
。
但深拷贝
会另外创造一个一模一样的对象,新对象跟原对象不共享内存
,修改新对象不会改到原对象。
题解
方法一:哈希表
利用哈希表的查询特点,考虑构建 原链表节点 和 新链表对应节点 的键值对映射关系,再遍历构建新链表各节点的 next 和 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)
{
//1、若头节点为空节点,直接返回nullptr ;
if(pHead==nullptr) return nullptr;
//2、初始化: 哈希表 , 节点 cur 指向头节点;
map<RandomListNode*, RandomListNode*> hashmap;
RandomListNode* cur=pHead;
//3、复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
while(cur){
hashmap[cur]=new RandomListNode(cur->label);//由上面RandomListNode的定义,初始化只需要初始化值,两个指针自动为NULL。
cur=cur->next;
}
cur=pHead;
// 4. 构建新链表的 next 和 random 指向
while(cur){
hashmap[cur]->next=hashmap[cur->next];
hashmap[cur]->random=hashmap[cur->random];
cur=cur->next;
}
// 5. 返回新链表的头节点
return hashmap[pHead];
}
};
时间复杂度 O(N): 两轮遍历链表,使用 O(N) 时间。
空间复杂度 O(N) : 哈希表 dic 使用线性大小的额外空间。
方法二:拼接 + 拆分(优化空间)
1、复制各节点,构建拼接链表:
2、构建新链表各节点的 random 指向:
当访问原节点 cur 的随机指向节点 cur.random 时,对应新节点 cur.next 的随机指向节点为 cur.random.next 。
3、拆分原 / 新链表:
设置 pre / res分别指向原 / 新链表头节点,遍历执行 pre.next = pre.next.next 和 res.next = res.next.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==nullptr) return nullptr;
RandomListNode* cur=pHead;
//1、复制各节点,构建拼接链表:
while(cur){
RandomListNode* temp=new RandomListNode(cur->label);
temp->next=cur->next;
cur->next=temp;
cur=temp->next;
}
//2、构建新链表各节点的 random 指向:
cur=pHead;
while(cur){
if(cur->random!=nullptr)
cur->next->random=cur->random->next;
cur=cur->next->next;
}
//3、拆分原 / 新链表:
RandomListNode* res_pHead=pHead->next;
RandomListNode* pre=pHead, *res=pHead->next;
while(res->next){
pre->next=pre->next->next;
res->next=res->next->next;
pre=pre->next;
res=res->next;
}
pre->next=nullptr;
return res_pHead;
}
};
面试题 52 两个链表的第一个公共节点
题目:
输入两个链表,找出它们的第一个公共节点。
如果两个链表没有交点,返回 null.
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
方法一:双指针法
题解:
定义两个指针 node1,node2 分别指向两个链表的头结点 headA,headB,然后同时分别逐结点遍历,任意一个指针遍历到末尾接着再从头遍历另一个链表。这样两个指针在两个链表长度不等的情况下都会遍历两个链表(即,两个指针遍历的长度相等),所以若有公共节点,一定会在这相遇,若没有则会在None的地方相遇;
这样,当它们相遇时,所指向的结点就是第一个公共结点。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* node1=headA;
ListNode* node2=headB;
while(node1!=node2){
//注node1==NULL与node1->next==NULL分别作为条件的区别:
//当有公共点时没区别,但用条件node1->next==NULL时,指针node1走不到NULL,所以,当无公共点时,此时会陷入死循环
node1==NULL? node1=headB : node1=node1->next;
node2==NULL? node2=headA : node2=node2->next;
// node1->next==NULL? node1=headB : node1=node1->next;
// node2->next==NULL? node2=headA : node2=node2->next;
}
return node1;
}
};
时间复杂度:O(M+N)。
空间复杂度:O(1)。
优化
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lenghtA=0,lenghtB=0;
ListNode* cur=headA;
while(cur){
++lenghtA;
cur=cur->next;
}
cur=headB;
while(cur){
++lenghtB;
cur=cur->next;
}
int n=0;
lenghtA>=lenghtB? n=lenghtA-lenghtB : n=lenghtB-lenghtA;
ListNode* nodeLong=headA;
ListNode* nodeShort=headB;
if(lenghtA>=lenghtB){
nodeLong=headA;
nodeShort=headB;
}
else{
nodeLong=headB;
nodeShort=headA;
}
for(int i=0;i<n;i++){
nodeLong=nodeLong->next;
}
while(nodeLong!=nodeShort){
nodeLong=nodeLong->next;
nodeShort=nodeShort->next;
}
return nodeLong;
}
};
时间复杂度:O(M+N)。
空间复杂度:O(1)。
面试题62 圆圈中最后剩下的数字
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
限制:
1 <= n <= 10^5
1 <= m <= 10^6
方法一:环形链表
- 用数组模拟圆圈(本方法超时,无法 AC,但思路可以参考)
代码:
class Solution {
public:
int lastRemaining(int n, int m) {
if(n==1 || m==1) return n-1;
vector<int> nums(n);
for(int i=0; i<n; i++){
nums[i]=i;
}
int count=1;
int index=0;
while(nums.size()>1){
if(count<m && index<=nums.size()-2){
count++;
index++;
}
else if(count<m && index==nums.size()-1){
count++;
index=0;
}
else if(count==m){
nums.erase(nums.begin()+index);
count=1;
}
}
return nums[0];
}
};
时间复杂度O(mn):没删除一个数字需m步,共删除n-1个数字;
空间复杂度O(n):用数组模拟圆圈
知识点
1、set.find(k) —>返回一个迭代器,指向第一个关键字为K的元素,若K不在容器中,则返回尾后迭代器。
2、深拷贝与浅拷贝
深拷贝(Deep Copy),类似我们常用的“复制粘贴”,与此对应的还有 浅拷贝,它们的区别是
:
浅拷贝
只复制指向某个对象的指针
,而不复制对象本身,新旧对象还是共享同一块内存
。
但深拷贝
会另外创造一个一模一样的对象,新对象跟原对象不共享内存
,修改新对象不会改到原对象。