19.链表相交
(力扣面试题 02.07. 链表相交)
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
图示两个链表在节点 c1
开始相交**:**
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
解题思路
题目要求找到两个单链表的第一个公共节点。核心思路是利用链表长度的差值来对齐链表尾部,从而让两个指针能够同时遍历并找到交点。
- 计算链表长度:
- 遍历链表 A 和链表 B,分别计算它们的长度
lenA
和lenB
。
- 遍历链表 A 和链表 B,分别计算它们的长度
- 对齐链表尾部:
- 如果链表 B 比链表 A 长,交换它们的长度和头指针,确保链表 A 是较长的链表。
- 计算长度差
gap = lenA - lenB
,让较长的链表(链表 A)的指针先走gap
步,这样两个链表的尾部对齐。
- 同时遍历链表:
- 从对齐后的起点开始,同时遍历链表 A 和链表 B。
- 如果两个指针相遇(即
curA == curB
),说明找到了第一个公共节点,直接返回该节点。 - 如果遍历完都没有相遇,则返回
NULL
,表示两个链表没有交点。
代码
#include <iostream>
#include <algorithm>
struct ListNode
{
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution
{
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
// 交点不是数值相等,而是指针相等
ListNode *curA = headA;
ListNode *curB = headB;
int lenA = 0;
int lenB = 0;
// 求链表A的长度
while (curA != NULL)
{
lenA++;
curA = curA->next;
}
// 求链表B的长度
while (curB != NULL)
{
lenB++;
curB = curB->next;
}
// 重新指向链表A和B的头节点 curA 和 curB现在是NULL
curA = headA;
curB = headB;
// 如果链表B比链表A长,交换它们的长度和头指针
if (lenB > lenA)
{
std::swap(lenA, lenB);
std::swap(curA, curB);
}
// 求长度差
int gap = lenA - lenB;
// 让curA先走gap步,这样两个链表的尾部对齐
while (gap--)
{
curA = curA->next;
}
// 同时遍历curA和curB
while (curA != NULL)
{
// 如果两个指针相遇,说明找到了第一个公共节点(返回指针)
if (curA == curB)
{
return curA;
}
// 移动链表的指针
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
- 时间复杂度:O(n + m)
- 空间复杂度:O(1)
39.逆波兰表达式求值
(力扣150题)
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
。 - 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
提示:
1 <= tokens.length <= 104
tokens[i]
是一个算符("+"
、"-"
、"*"
或"/"
),或是在范围[-200, 200]
内的一个整数
逆波兰表达式:
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
- 平常使用的算式则是一种中缀表达式,如
( 1 + 2 ) * ( 3 + 4 )
。 - 该算式的逆波兰表达式写法为
( ( 1 2 + ) ( 3 4 + ) * )
。
逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成
1 2 + 3 4 + *
也可以依据次序计算出正确结果。 - 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
解题思路
- 初始化栈:
- 使用一个栈(
stack<long long>
)来存储操作数。栈的类型为long long
,以支持较大的整数运算。
- 使用一个栈(
- 遍历表达式:
- 遍历 RPN 表达式的每个元素(
tokens
)。每个元素可以是数字或运算符。
- 遍历 RPN 表达式的每个元素(
- 处理数字:
- 如果当前元素是数字(即不是运算符),将其转换为
long long
类型并压入栈中。这里使用std::stoll
函数将字符串转换为数字。
- 如果当前元素是数字(即不是运算符),将其转换为
- 处理运算符:
- 如果当前元素是运算符(
+
,-
,*
,/
),需要从栈中弹出两个操作数。 - 检查栈中是否有足够的操作数(至少两个)。如果不足,说明表达式不合法,打印错误信息并返回错误码
0
。 - 弹出栈顶的两个操作数(
num1
和num2
),注意顺序:num1
是栈顶元素,num2
是第二个元素。 - 根据运算符对两个操作数进行运算,并将结果压入栈中。
- 如果当前元素是运算符(
- 检查最终结果:
- 遍历结束后,检查栈中是否只剩下一个元素。如果栈中不止一个元素,说明表达式不合法,打印错误信息并返回错误码
0
。 - 如果栈中只剩下一个元素,该元素即为 RPN 表达式的结果。
- 遍历结束后,检查栈中是否只剩下一个元素。如果栈中不止一个元素,说明表达式不合法,打印错误信息并返回错误码
- 返回结果:
- 弹出栈顶元素并返回其值。
代码实现的逻辑
- 栈的使用:栈用于存储操作数,支持后进先出(LIFO)的操作,非常适合处理 RPN 表达式。
- 错误处理:通过检查栈的大小来确保每次运算符操作时有足够的操作数,避免运行时错误。
- 最终检查:确保栈中只剩下一个元素,验证 RPN 表达式的合法性
#include <iostream>
#include <stack>
#include <string>
#include <vector>
#include <cstdlib>
using namespace std;
class Solution
{
public:
int evalRPN(vector<string> &tokens)
{
// 栈
stack<long long> st;
for (int i = 0; i < tokens.size(); i++)
{
// 如果遍历到运算符
if (tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/")
{
// 检查栈中是否有足够的元素
if(st.size() < 2)
{
perror("Error: Not enough operands for the operator");
return 0;
}
// 获取栈顶元素 就是要运算的数字
long long num1 = st.top();
// 弹出栈
st.pop();
long long num2 = st.top();
st.pop();
// 计算数值 再加入栈里面
if (tokens[i] == "+")
{
st.push(num2 + num1);
}
if (tokens[i] == "-")
{
st.push(num2 - num1);
}
if (tokens[i] == "*")
{
st.push(num2 * num1);
}
if (tokens[i] == "/")
{
st.push(num2 / num1);
}
}
// 如果遍历到数字 加入栈
else
{
// std::stoll,表示 “string to long long”,也就是将字符串转换为 long long 类型的整数。
st.push(std::stoll(tokens[i]));
}
}
// 如果最后结果不是一个元素 表达式不合法
if(st.size() != 1)
{
perror("Error: Invalid RPN expression");
return 0;
}
// 统计最后的数值 此时栈就一个元素(结果)
long long result = st.top();
// 弹出
st.pop();
return result;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(n)
40.滑动窗口最大值
(力扣239题)
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
1 <= k <= nums.length
解题思路
本题的目标是求解滑动窗口中的最大值。滑动窗口的大小为 k
,随着窗口的滑动,我们需要高效地找到每个窗口的最大值。为了实现这一目标,我们使用了一个自定义的单调队列(Mydeque
)来维护窗口内的元素。
单调队列的特性
- 单调递减:队列中的元素从大到小排列。队列头部始终是当前窗口的最大值。
- 高效移除:当窗口滑动时,窗口外的元素需要被移除。通过
pop
方法,我们检查队列头部是否是窗口外的元素,如果是,则移除。 - 动态维护:当新元素加入窗口时,通过
push
方法,将队列尾部所有小于新元素的值移除,确保队列始终保持单调递减。
算法步骤
- 初始化窗口:将前
k
个元素加入单调队列。 - 记录第一个窗口的最大值:队列头部即为第一个窗口的最大值。
- 滑动窗口:
- 移除窗口外的元素:通过
pop
方法,移除队列头部的窗口外元素。 - 加入新元素:通过
push
方法,将新元素加入队列,并维护队列的单调性。 - 记录当前窗口的最大值:队列头部即为当前窗口的最大值。
- 移除窗口外的元素:通过
- 返回结果:所有窗口的最大值存储在结果数组中。
#include <iostream>
#include <deque>
#include <vector>
class Solution
{
// 单调队列(从大到小)
private:
class Mydeque
{
public:
// 使用deque来实现单调队列
std::deque<int> que;
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则
// 同时pop之前判断队列当前是否为空
void pop(int value)
{
if (!que.empty() && value == que.front())
{
que.pop_front();
}
}
void push(int value)
{
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
while (!que.empty() && value > que.back())
{
// 列尾部的值在滑动窗口中不再有用,可以移除。
que.pop_back();
}
// 没有的话就单纯把值添加到单调队列里
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
int front()
{
return que.front();
}
};
public:
std::vector<int> maxSlidingWindow(std::vector<int> &nums, int k)
{
Mydeque que;
std::vector<int> result;
// 先将前k的元素放进队列
for (int i = 0; i < k; i++)
{
que.push(nums[i]);
}
// result 记录前k的元素的最大值
result.push_back(que.front());
for (int i = k; i < nums.size() ; i++)
{
// 滑动窗口移除最前面元素
que.pop(nums[i - k]);
// 滑动窗口前加入最后面的元素
que.push(nums[i]);
// 记录对应的最大值
result.push_back(que.front());
}
return result;
}
};
时间复杂度
每个元素最多被加入和移除队列一次,因此时间复杂度为 O(n),其中 n 是数组的长度。
空间复杂度
单调队列的空间复杂度为 O(k),因为队列中最多存储窗口大小的元素。
通过单调队列,我们能够高效地解决滑动窗口的最大值问题,避免