文章目录
剑指offer
前言
主要刷题平台为 牛客网,部分题目使用 LeetCode 和 ACwing 作为辅助。每题均包含主要思路、详细注释、时间复杂度和空间复杂度分析,每题均是尽可能最佳的解决办法。
机器人的运动范围
题目:
地上有一个 rows 行和 cols 列的方格。坐标从 [0,0] 到 [rows-1,cols-1] 。一个机器人从坐标 [0,0] 的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 threshold 的格子。 例如,当 threshold 为 18 时,机器人能够进入方格 [35,37] ,因为 3+5+3+7 = 18。但是,它不能进入方格 [35,38] ,因为 3+5+3+8 = 19 。请问该机器人能够达到多少个格子?
思路:
宽度优先遍历的经典题目
要注意判断障碍物,有些格子是不能走的
class Solution {
public:
int get_signal_sum(int x)
{
int s=0;
while(x)
{
s+=x%10;
x/=10;
}
return s;
}
int get_sum(pair<int, int> &p)
{
return get_signal_sum(p.first)+get_signal_sum(p.second);
}
int movingCount(int threshold, int rows, int cols) {
int res=0;
if(rows==0||cols==0)
{
return 0;
}
//默认所有格子都是没有走过的
vector<vector<bool>> st(rows,vector<bool>(cols));
queue<pair<int, int>> q; //宽度优先搜索使用的队列
//也可以使用 make_pair
q.push({0,0});
//走的方向: 纵为x 横为y 上右下左
int dx[4]={-1,0,1,0};
int dy[4]={0,1,0,-1};
//循环放入
while(q.size())
{
auto t=q.front();
q.pop();
//如果不符合条件, 就不走这条路
if(get_sum(t)>threshold||st[t.first][t.second]==true)
{
continue;
}
res++;
st[t.first][t.second]=true; //表示走过了
//走的方向
for(int i=0;i<4;i++)
{
int x=t.first+dx[i];
int y=t.second+dy[i];
//判断是否合法的坐标
if(x>=0&&x<rows&&y>=0&&y<cols)
{
q.push({x,y});
}
}
}
return res;
}
};
每个节点最多只会入队一次,所以时间复杂度不会超过方格中的节点个数。
最坏情况下会遍历方格中的所有点,所以时间复杂度就是 O(nm)O(nm)。
剪绳子
题目:
给你一根长度为 n 的绳子,请把绳子剪成整数长的 m 段( m 、 n 都是整数, n > 1 并且 m > 1 , m <= n ),每段绳子的长度记为 k[1],…,k[m] 。请问 k [ 1 ] ∗ k [ 2 ] ∗ . . . ∗ k [ m ] k[1]*k[2]*...*k[m] k[1]∗k[2]∗...∗k[m] 可能的最大乘积是多少?例如,当绳子的长度是 8 时,我们把它剪成长度分别为 2、3、3 的三段,此时得到的最大乘积是 18 。
数据范围: 2 ≤ n ≤ 60 2 \le n \le 60 2≤n≤60
进阶:空间复杂度 O ( 1 ) O(1) O(1) ,时间复杂度 O ( n ) O(n) O(n)
思路:
先说结论:尽量分出多个3,其余为2,这样构成的数最大
下面证明:
有
N
N
N,可以划分为
N
=
n
1
+
n
2
+
.
.
.
n
k
N=n_1+n_2+...n_k
N=n1+n2+...nk
先假设存在
n
i
>
=
5
n_i>= 5
ni>=5, 有
3
∗
(
n
i
−
3
)
>
=
n
i
3*(n_i-3)>=n_i
3∗(ni−3)>=ni, 即
3
n
i
−
9
>
=
n
i
3n_i-9>=n_i
3ni−9>=ni, 就可以得到
2
n
i
>
=
9
2n_i>=9
2ni>=9, 这是必然成立的,因为我们已经假设过
n
i
>
=
5
n_i>= 5
ni>=5了,也就是说 如果最终解 中包含 大于等于5的数字,则一定可以拆分出 3,那么5就一定不是最优解了,下面最优解就落在 2 3 4 中了
下面假设 n i = 4 n_i=4 ni=4, 那么 n i = 2 ∗ 2 n_i=2*2 ni=2∗2,所以可以使用 2 ∗ 2 2*2 2∗2 来替代,所以最优解就一定在 2 和 3 之间了
而且有 2 ∗ 2 ∗ 2 < 3 ∗ 3 2*2*2<3*3 2∗2∗2<3∗3, 即如果有三个以上的2,替换成3的乘积就一定更大,所以 2 的数量一定不会超过两个
选用尽量多的3,直到剩下2或者4时,用2。如果N模3余1就是两个2,如果模3余2就是一个2,如果模3余0就没有2
class Solution {
public:
int cutRope(int number) {
if(number<=3)
{
return 1*(number-1);
}
int res=1;
if(number%3==1)
{
//得到两个2
res*=4;
number-=4;
}
if(number%3==2)
{
//得到一个2
res*=2;
number-=2;
}
//这个循环可以省掉
if(number%3==0)
{
//没有2
res*=3;
number-=3;
}
//其余全是3
while(number)
{
res*=3;
number-=3;
}
return res;
}
};
有一定的数学基础很重要
时间复杂度分析:当 n n n 比较大时, n n n 会被拆分成 ⌈ n / 3 ⌉ ⌈n/3⌉ ⌈n/3⌉ 个数,我们需要计算这么多次减法和乘法,所以时间复杂度是 O ( n ) O(n) O(n)。
二进制中1的个数
题目:
输入一个整数 n ,输出该数32位二进制表示中1的个数。其中负数用补码表示。
数据范围: − 2 31 < = n < = 2 31 - 2^{31} <= n <= 2^{31} −231<=n<=231
即范围为: − 2147483648 < = n < = 2147483647 -2147483648<= n <= 2147483647 −2147483648<=n<=2147483647
思路:
感觉这题主要是在考察语言的基本知识,对于 位移操作,二进制的表示一定要熟悉
class Solution {
public:
int NumberOf1(int n) {
//将负数转换为整数,变为无符号数,二进制不变化,但是在解释这个二进制的时候就是以无符号解释的
unsigned int _n=n;
//因为在C++ 中如果是负数就会在每次右移后 在最高位补1,为了防止 补1,就事先将其转化为 无符号数了
int s=0; //无符号数最高位 补0
while(_n)
{
//每次取出个位,如果是1就加,如果不是就不加
s+=_n&1;
//右移一位,去掉个位
_n>>=1;
}
return s;
}
};
数值的整数次方
题目:
实现函数 double Power(double base, int exponent),求base的exponent次方。
注意:
1.保证base和exponent不同时为0。
2.不得使用库函数,同时不需要考虑大数问题
3.有特殊判题,不用考虑小数点后面0的位数。
数据范围: $|base| \le 100 $ , ∣ e x p o n e n t ∣ ≤ 100 |exponent| \le 100 ∣exponent∣≤100,保证最终结果一定满足 ∣ v a l ∣ ≤ 1 0 4 |val| \le 10^4 ∣val∣≤104
进阶:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)
思路:
下面这个算法的 时间复杂度 和 空间复杂度 都是满足要求的,也是比较简单和容易实现的
class Solution {
public:
double Power(double base, int exponent) {
//用res 来实现 求指数
double res=1;
//防止 exponent 为负数
for(int i=0;i<abs(exponent);i++)
{
res*=base;
}
//如果 指数 是负数,就要将其变为 分数输出
if(exponent<0)
{
res = 1/res;
}
return res;
}
};
空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)
从1到n的位数
题目:
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
- 用返回一个整数列表来代替打印
- n 为正整数,0 < n <= 5
感觉这一题简单的莫名其妙的
简单是由于 牛客网 的题目限制了 输入范围,导致不会出现大数溢出的问题,如果考虑大数移除的问题,就是如 力扣 的要求:最大的n位数 ,就需要使用 数组模拟遍历了
class Solution { public: vector<int> output; void permutation(string& s, int length, int pos) { // 对带有循环的递归有时候还挺绕的 if(pos == length) { //达到位数要求了 inputNumbers(s); return; } for(int i=0; i<=9; ++i) { //对于每一位 每个数字都有可能 s[pos] = i + '0'; permutation(s, length, pos + 1); } } void inputNumbers(string s) { // 放入答案 bool isUnwantedZero = true; string temp = ""; for(int i=0; i<s.length(); ++i) { if(isUnwantedZero && s[i] != '0') isUnwantedZero = false; if(!isUnwantedZero) temp += s[i]; } //转化为数字放入 if(temp != "") output.push_back(stoi(temp)); // 如果 s = "000",则temp会是空,stoi无法执行,会报错 } vector<int> printNumbers(int n) { if(n <= 0) return vector<int>(0); //对于大数的求解 string s(n, '0'); for(int i=0; i<=9; ++i) { //int-> char 的转换 s[0] = i + '0'; permutation(s, s.length(), 1); } return output; } };
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param n int整型 最大位数
* @return int整型vector
*/
vector<int> printNumbers(int n) {
// write code here
vector<int> result;
if(n==1)
{
for(int i=1;i<=9;i++)
{
result.push_back(i);
}
}
else if(n==2)
{
for(int i=1;i<=99;i++)
{
result.push_back(i);
}
}
else if(n==3)
{
for(int i=1;i<=999;i++)
{
result.push_back(i);
}
}
else if(n==4)
{
for(int i=1;i<=9999;i++)
{
result.push_back(i);
}
}
else
{
for(int i=1;i<=99999;i++)
{
result.push_back(i);
}
}
return result;
}
};
移除链表元素
题目:
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。
1.此题对比原题有改动
2.题目保证链表中节点的值互不相同
3.该题只会输出返回的链表和结果做对比,所以若使用 C 或 C++ 语言,你不需要 free 或 delete 被删除的节点
数据范围:
0<=链表节点值<=10000
0<=链表长度<=10000
思路:
这里的要求是降低了好多的,没有让 再释放内存,感觉还是力扣的很不错: 移除链表元素
还有要注意一点就是 使用虚拟头结点 要注意在进循环的时候是 cur -> next !=nullptr
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) : val(x), next(nullptr) {}
* };
*/
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @param val int整型
* @return ListNode类
*/
ListNode* deleteNode(ListNode* head, int val) {
// write code here
ListNode* dummyHead=new ListNode(0);
dummyHead->next=head;
ListNode* cur=dummyHead;
//ListNode* tmp=nullptr;
while(cur->next!=nullptr)
{
//tmp=cur->next;
if(cur->next->val==val)
{
cur->next=cur->next->next;
//delete tmp;
}
else
{
cur=cur->next;
}
}
return dummyHead->next;
}
};
acwing类似题目: O ( 1 ) O(1) O(1)时间内删除节点
给定单向链表的一个节点指针,定义一个函数在O(1)时间删除该结点。
假设链表一定存在,并且该节点一定不是尾节点
个人感觉这是一种非常取巧的方法,由于只有一个节点,而且是单向链表,无法获取 前驱结点,所以就采用了非常巧妙的办法:既然当前节点需要使用到 前驱结点,那我就使用有 前驱结点 的点进行删除,我们使用 当前节点的下一个结点的值 覆盖当前节点,那么相当于当前节点就已经被删除了,我们在处理下一个结点就可以了(下一个结点的前驱结点就是当前节点),问题就很容易解决了
/**
* 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; //删除后面的结点
}
};
删除链表中重复的结点
题目:
在一个排序的链表中,存在重复的节点,请删除该链表中重复的节点,重复的节点不保留。
数据范围
链表中节点 val 值取值范围 [ 0 , 100 ] [0,100] [0,100]。
链表长度 [ 0 , 100 ] [0,100] [0,100]
思路:
这里题目要求的是 不保留 重复的头结点,所以这里就使用一个暂时的结点,保存所有相同的结点,然后进行删除操作
而且本题在 牛客网 的《剑指offer》题单中没有出现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* deleteDuplication(ListNode* head) {
// ListNode* fast=head;
// ListNode* slow=head;
// while(fast!=nullptr)
// {
// //保留重复节点,就是只删除多余的结点
// if(fast->val==slow->val)
// {
// while(fast->val==slow->val)
// {
// fast=fast->next;
// }
// slow->next=fast;
// }
// fast=fast->next;
// slow=slow->next;
// }
// return head;
//可能会把头结点删掉,所以这里使用一个 虚拟头结点
auto dummyHead=new ListNode(0);
dummyHead->next=head;
ListNode* cur=dummyHead;
while(cur->next!=nullptr)
{
//记录相同的结点
ListNode* tmp=cur->next;
while(tmp!=nullptr&&cur->next->val==tmp->val)
{
tmp=tmp->next;
}
//只有一个节点
if(cur->next->next==tmp)
{
cur=cur->next;
}
else
{
//有多个相同的结点,就将一整段 直接删掉
cur->next=tmp;
}
}
return dummyHead->next;;
}
};
*正则表达式匹配
题目:
请实现一个函数用来匹配包括’.‘和’*'的正则表达式。
1.模式中的字符’.'表示任意一个字符
2.模式中的字符’*'表示它前面的字符可以出现任意次(包含0次)。
在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配
数据范围:
1.str 只包含从 a-z 的小写字母。
2.pattern 只包含从 a-z 的小写字母以及字符 . 和 ,无连续的 ''。
- 0 ≤ s t r . l e n g t h ≤ 26 0 \le str.length \le 26 0≤str.length≤26
- 0 ≤ p a t t e r n . l e n g t h ≤ 26 0 \le pattern.length \le 26 0≤pattern.length≤26
思路:
经典动态规划问题:
确定dp
数组:dp[i][j]
表示 s[i,...]
和p[j,...]
均相匹配,就是s
从i
向后和 p
从j
向后都是匹配的
初始化dp
数组:f[n][m]=true
, 表示初始时都是匹配的
确定递推公式:
p[j]
是正常字段 f[i][j]=s[i]==p[j]&&f[i+1][j+1]
,就是 f[i][j]
就只与 s[i]
和p[j]
是否匹配有关了,并且以后的也都是匹配的
p[j]
是.
f[i][j]=f[i+1][j+1]
,因为 .
表示任何字符,所以必然匹配,只要后面的匹配就可以了
p[j+1]
是*
f[i][j]=f[i][j+2]||f[i][j]=f[i+1][j]
, 因为 如果只匹配了 0 个字符,表示第j
个字符不应该出现,就应该跳过j
和 j+1
两个字符,与后面的匹配,如果包含其他多个字符,就只要满足和 s[i]
的下一个字符匹配的长度就可以了,所以是s[i+1]
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param str string字符串
* @param pattern string字符串
* @return bool布尔型
*/
int n,m;
vector<vector<int>> dp;
string s,p;
bool match(string str, string pattern) {
// write code here
s=str;
p=pattern;
n=s.size();
m=p.size();
dp=vector<vector<int>> (n+1,vector<int>(m+1,-1));
return Isdp(0,0);
}
bool Isdp(int x,int y)
{
if(dp[x][y]!=-1)
{
return dp[x][y];
}
//最后一个元素
if(y==m)
{
if(x==n)
{
return true;
}
else
{
return false;
}
//return dp[x][y]=x==n;
}
bool first_match=x<n&&(p[y]=='.'||s[x]==p[y]);
bool ans;
if(y+1<m && p[y+1]=='*')
{
ans=Isdp(x, y+2) || first_match && Isdp(x+1,y);
}
else
{
ans=first_match&&Isdp(x+1,y+1);
}
return dp[x][y]=ans;
}
};
很难搞,没有什么思路,代码也写不出来
时间复杂度分析: n n n 表示 s 的长度, m m m 表示 p 的长度,总共 n m nm nm 个状态,状态转移复杂度 O ( 1 ) O(1) O(1),所以总时间复杂度是 O ( n m ) O(nm) O(nm)
*表示数值的字符串
题目:
请实现一个函数用来判断字符串str是否表示数值(包括科学计数法的数字,小数和整数)。
科学计数法的数字(按顺序)可以分成以下几个部分:
- 若干空格
- 一个整数或者小数
- (可选)一个 ‘e’ 或 ‘E’ ,后面跟着一个整数(可正可负)
- 若干空格
小数(按顺序)可以分成以下几个部分:
- 若干空格
- (可选)一个符号字符(‘+’ 或 ‘-’)
- 可能是以下描述格式之一:
- 至少一位数字,后面跟着一个点 ‘.’
- 至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字
- 一个点 ‘.’ ,后面跟着至少一位数字
- 若干空格
整数(按顺序)可以分成以下几个部分:
- 若干空格
- (可选)一个符号字符(‘+’ 或 ‘-’)
- 至少一位数字
- 若干空格
思路:
按照要求 模拟所有可能的情况
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param str string字符串
* @return bool布尔型
*/
bool isNumeric(string str) {
//去除额外空格
int i=0,j=str.size()-1;
while(i<str.size()&&str[i]==' ')
{
i++;
}
while(j>=0&&str[j]==' ')
{
j--;
}
if(i>j)
{
//去除空格之后 为空,就直接返回 false
return false;
}
//截取去除空格后的字符串
str=str.substr(i,j-i+1);
if(str[0]=='+'||str[0]=='-')
{
//截取掉 前面的符号位
str=str.substr(1);
}
if(str.empty()||str[0]=='.'&& str.size()==1)
{
//如果为空,只有符号,这些都不满足 定义
// if(str[0]>='0'&&str[0]<='9')
// {
// return true;
// }
return false;
}
int dot=0,e=0;
for(int i=0;i<str.size();i++)
{
if(str[i]>='0'&&str[i]<='9')
{
continue;
}
else if(str[i]=='.')
{
dot++;
if(dot>1||e)
{
//出现了多个点 或者在 点 的前面出现了e
return false;
}
}
else if(str[i]=='e'||str[i]=='E')
{
e++;
//e 的前面后面都要有数字
if(!i || i+1==str.size()||e>1||str[i-1]=='.'&&i==1)
{
return false;
}
if(str[i+1]=='+'||str[i+1]=='-')
{
if(i+2==str.size())
{//在e后面的符号 是最后一位了
return false;
}
i++;
}
}
else
{
return false;
}
}
//其他可能的情况
return true;
}
};
调整数组顺序
题目:
输入一个长度为 n 整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前面部分,所有的偶数位于数组的后面部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
数据范围: 0 ≤ n ≤ 50000 0 \le n \le 50000 0≤n≤50000,数组中每个数的值 0 ≤ v a l ≤ 100000 0 \le val \le 100000 0≤val≤100000
要求:时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)
进阶:时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( 1 ) O(1) O(1)
思路:
这里牛客网的有个约束条件, 要求保证 保证奇数和奇数,偶数和偶数之间的相对位置不变,所以就不能简单的考虑使用双指针思想了,这里使用的是 冒泡排序的思想,将 奇数 和 偶数 进行交换
这个条件在 《剑指offer》这本书中是没有的,所以 acwing上的 调整数组顺序 就没有这个约束条件,就可以直接使用 基于 快速排序思想的双指针做法,如下:
class Solution { public: bool isOdd(int num) { if (num % 2 == 1) { return true; } return false; } bool isEnev(int num) { if (num % 2 == 0) { return true; } return false; } void reOrderArray(vector<int> &array) { if(array.size()==0) { return; } //基于快速排序思想 int left=-1,right=array.size(); while(left<right) { do left++ ; while(isOdd(array[left])); do right--; while(isEnev(array[right])); if(left<right) { swap(array[left],array[right]); } } } };
还可以写的更加简略:
class Solution { public: void reOrderArray(vector<int> &array) { int l = 0, r = array.size() - 1; while (l < r) { while (l < r && array[l] % 2 == 1) l ++ ; while (l < r && array[r] % 2 == 0) r -- ; if (l < r) swap(array[l], array[r]); } } };
当两个指针相遇时,走过的总路程长度是 n n n,所以时间复杂度是 O ( n ) O(n) O(n)。
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param array int整型vector
* @return int整型vector
*/
vector<int> reOrderArray(vector<int>& array) {
// write code here
int size=array.size();
//基于 冒泡排序的思想
for(int i=0;i<size-1;i++)
{
for(int j=0;j<size-1-i;j++)
{
//奇数 偶数 进行交换
if(array[j]%2==0&&array[j+1]%2!=0)
{
swap(array[j],array[j+1]);
}
}
}
return array;
}
};
时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( 1 ) O(1) O(1)
链表中倒数第k个节点
题目:
输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。
如果该链表长度小于k,请返回一个长度为 0 的链表。
数据范围: 0 ≤ n ≤ 1 0 5 0 \leq n \leq 10^5 0≤n≤105,$0 \leq a_i \leq 10^9, 0 ≤ k ≤ 1 0 9 0 \leq k \leq 10^9 0≤k≤109
要求:空间复杂度 O ( n ) O(n) O(n),时间复杂度 O ( n ) O(n) O(n)
进阶:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)
思路:
比较容易实现,使用双指针法,先让 快指针 走 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* FindKthToTail(ListNode* pHead, int k) {
// write code here
//防止空链表
if(pHead==nullptr)
{
return nullptr;
}
ListNode* fast=pHead;
ListNode* slow=pHead;
//快指针先走k步
while(k--)
{
//如果链表长度小于k,那么fast指针就越界了
if(fast==nullptr)
{
return nullptr;
}
fast=fast->next;
}
//快慢指针 再同时走
while(fast!=nullptr)
{
slow=slow->next;
fast=fast->next;
}
//返回慢指针
return slow;
}
};
时间复杂度 O ( N ) O(N) O(N):N为链表长度,遍历整个链表
空间复杂度 O ( 1 ) O(1) O(1):使用额外常数大小空间
链表的环
题目:
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
数据范围: n ≤ 10000 n\le10000 n≤10000, 1 < = 结 点 值 < = 10000 1<=结点值<=10000 1<=结点值<=10000
要求:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
*/
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
ListNode* fast=pHead;
ListNode* slow=pHead;
while(fast!=nullptr&&fast->next!=nullptr) //fast->next!=nullptr可以防止只有一个节点的情况
{
//如果快慢指针相遇,则一定是在环内相遇的
slow=slow->next;
fast=fast->next->next;
//如果快慢指针相遇,就说明有环,就要找 环的入口
if(fast==slow)
{
//根据公式可以推倒
ListNode* index1=fast; //相遇处
ListNode* index2=pHead; //链表头
while(index1!=index2)
{
index1=index1->next;
index2=index2->next;
}
return index2; //返回环的入口
}
}
return nullptr;
}
};
时间复杂度: O ( n ) O(n) O(n) 空间复杂度: O ( 1 ) O(1) O(1)
反转链表
题目:
给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
数据范围: 0 ≤ n ≤ 10000 0\leq n\leq10000 0≤n≤10000
要求:空间复杂度 O ( 1 ) O(1) O(1) ,时间复杂度 O ( n ) O(n) O(n) 。
思路:
反转链表可以说是在链表中 出场率很高的题目了,一定要熟练掌握
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
ListNode* pre=nullptr;
ListNode* cur=pHead;
ListNode* tmp=nullptr;
while(cur!=nullptr)
{
//保存下一个结点
tmp=cur->next;
//重复这个过程
cur->next=pre;
//原地改变指针指向
pre=cur;
cur=tmp;
}
return pre;
}
};
时间复杂度:
O
(
n
)
O(n)
O(n), 遍历一次链表
空间复杂度:
O
(
1
)
O(1)
O(1)
合并链表
题目:
输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
数据范围: 0 ≤ n ≤ 10000 0 \le n \le 10000 0≤n≤10000, − 1000 ≤ 节 点 值 ≤ 1000 -1000 \le 节点值 \le 1000 −1000≤节点值≤1000
要求:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)
归并排序的思想,由于两个链表是有序的,所以可以直接开辟一个新的目标链表,每次放入这两个链表中较小的一个。最后如果某个链表有剩余,就直接将其放入。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
//使用虚拟头结点
ListNode* dummyHead=new ListNode(0);
ListNode* cur=dummyHead;
//归并排序的思想
while(pHead1!=nullptr&&pHead2!=nullptr)
{
//放入小的结点
if(pHead1->val<=pHead2->val)
{
cur->next=pHead1;
pHead1=pHead1->next;
}
else
{
cur->next=pHead2;
pHead2=pHead2->next;
}
cur=cur->next;
}
//将剩余的未放入的结点继续放入
if(pHead1!=nullptr)
{
cur->next=pHead1;
pHead1=pHead1->next;
}
if(pHead2!=nullptr)
{
cur->next=pHead2;
pHead2=pHead2->next;
}
//记得不要反悔虚拟头结点
return dummyHead->next;
}
};
两个链表各遍历一次,所以时间复杂度为 O ( n ) O(n) O(n)