1.力扣 回文数
回文数:将数字进行反转,然后将反转后的数字与原始数字进行比较,如果它们是相同的,那么这个数字就是回文。
思路:取数据的后半部分进行反转,然后与前半部分进行比较,如果一样,则是回文数,反之,不是。
例如,输入 1221,我们可以将数字 “1221” 的后半部分从 “21” 反转为 “12”,并将其与前半部分 “12” 进行比较,因为二者相同,我们得知数字 1221 是回文。
算法:首先,我们应该处理一些临界情况。所有负数都不可能是回文,例如:-123 不是回文,因为 - 不等于 3。所以我们可以对所有负数返回 false。除了 0 以外,所有个位是 0 的数字不可能是回文,因为最高位不等于 0。所以我们可以对所有大于 0 且个位是 0 的数字返回 false。
对于数字 1221,如果执行 1221 % 10,我们将得到最后一位数字 1,要得到倒数第二位数字,我们可以先通过除以 10 把最后一位数字从 1221 中移除,1221 / 10 = 122,再求出上一步结果除以 10 的余数,122 % 10 = 2,就可以得到倒数第二位数字。如果我们把最后一位数字乘以 10,再加上倒数第二位数字,1 * 10 + 2 = 12,就得到了我们想要的反转后的数字。如果继续这个过程,我们将得到更多位数的反转数字。
由于整个过程我们不断将原始数字除以 10,然后给反转后的数字乘上 10,所以,当原始数字小于或等于反转后的数字时,就意味着我们已经处理了一半位数的数字了。
2.力扣 罗马数字转整数
解题思路:
1、从右往左反向遍历
2、对比当前数字Num(i)和Num(i+1)的大小
3、正常情况下左边比右边大(或相等), 则做“加法”(右边第一个数字永远做加法)
4、如果遇到特殊情况(左边比右边小),则做“减法”
例如MCMXCIV,从右往左依次遍历:
V +5 第一个数字,永远做加法
I -1 比右边小,做减法
C +100 比右边大,做加法
X -10 比右边小,做减法
M +1000 比右边大,做加法
C -100 比右边小,做减法
M +1000 比右边大,做加法
结果全部累加,即可得到答案1994。
3.最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀,如果不存在公共前缀,返回空字符串 ""
。
思路:先进行排序,后比较头尾
sort()函数是C++一种排序方法之一,它使用的排序方法是类似于快排的方法(既有快速排序又有与其它排序方法的结合),时间复杂度为n*log2(n),包含在头文件为 #include”algorithm”。
sort函数有三个参数:
1.第一个是要排序的起始地址。
2.第二个是要排序的结束地址。
3.第三个参数是排序的方法,默认的排序方法是从小到大排序。
C++中两个字符串字节进行比较时,是把字符串的每个字符从首端开始进行按照ASCII值的大小进行比较,如果相同,依次往下比较。
string a="abcd";
1.获取字符串最后一个字符
auto b=a.back(); //结果为 b='d';
2.修改字符串最后一个字符
a.back()='!'; //结果为 a="abc!";
3.获取字符串第一个字符
auto b=a.front(); //结果为 b='a';
4.修改字符串第一个字符
a.front()='!'; //结果为 a="!bcd";
4.两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值的那两个整数,并返回它们的数组下标。
vector的概念与性质:vector是顺序容器,本质是动态数组。创建vector容器时需要说明该容器内元素的类型。如:vector<int> twoSum;
vector容器在做形参传递时,可以采用以下三种方式:
void init_vector1(vector<int>vectest);
void init_vector2(vector<int>&vectest);
void init_vector3(vector<int>*vectest);
init_vector1中是值传递,形参的改变不会对实参有影响,并且会调用vector的拷贝构造函数将实参的值复制给形参。init_vector2和init_vector3分别是引用传递和指针传递,它们会对实参做出影响。
建议在实际应用中采用引用传递。
vector<int> twoSum(vector<int>& nums, int target);
参考文献:
[1]Vector容器
[2]c++中vector做形参传递的问题
i++先赋值后运算,++i先运算后赋值。++i的效率比i++高。因为i++先赋值后运算,因此要多生成一个局部对象
哈希表
哈希表是一个采用键值和值相互对应的函数,在c++中,与哈希表对应的容器是unordered_map(无序容器)。建立哈希表
unordered_map<int,int>hashtable;
该句建立一个名为hashtable的,键值对为<int,int>类型的unordered_map容器。其中<int,int>是指键值对类型,前者是键的类型,后者是值的类型。
auto it = hashtable.find(target - nums[i]);
find(key)是unordered_map容器中寻找键key对应的值的成员方法。若键key与其值的键值对在容器中存在,则返回一个指向该键值对的正向迭代器,反之则返回一个指向容器中最后一个键值对之后位置的迭代器。
if(it != hashtable.end())
该句判断是否存在我们需要的键值对。成员方法end()生成指向容器中最后一个键值对之后位置的迭代器。
参考文献:unordered_map容器
auto标识符
1、auto就是根据变量值推断变量类型。
2、使用auto时必须初始化变量。
3、auto是一个占位符,并不是一个类型,因此无法进行类型转换之类的操作。
参考文献:auto标识符
迭代器iterator
问、读写容器中的元素,需要使用迭代器iterator。容器可以看作为吉他,迭代器可以看作为拨片,吉他(容器)是声音(元素)的载体,但是使吉他发出声音(能够访问到容器的元素)需要使用拨片(迭代器)。
迭代器根据访问方式分为正向迭代器、双向迭代器、随机访问迭代器。不同的访问方式使得迭代器可以做的运算不同。
迭代器常用功能:
1、定义一个正向迭代器
容器类名::iterator 迭代器名; vector<int>::iterator i;
2、取得迭代器所指元素 a = *i;
3、移动迭代器,访问下一个元素(移动方法视迭代器类别变化) ++i;
在unordered_map容器中,迭代器指向键值对,即指向两个元素,键和值。因此通过迭代器访问unordered_map容器时,需要说明访问的是哪个元素。其中first是键,second是值。
return {it->second,i};
上句即为访问键值对中的值。
参考文献:迭代器
方法一:双指针
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int i = 0, j = nums.size() - 1;
while(i < j){
if(target - nums[i] < nums[j]) j--;
else if(target - nums[i] > nums[j]) i++;
else return{nums[i], nums[j]};
}
return {};
}
};
和为s的连续正数序列
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
方法一:求和公式
class Solution {
public:
vector<vector<int>> findContinuousSequence(int target) {
int i = 1;
double j = 2.0;
vector<vector<int>> res;
while(i < j) {
j = (-1 + sqrt(1 + 4 * (2 * target + (long) i * i - i))) / 2;
if(i < j && j == (int)j) {
vector<int> ans;
for(int k = i; k <= (int)j; k++)
ans.push_back(k);
res.push_back(ans);
}
i++;
}
return res;
}
};
方法二:滑动窗口(双指针)
class Solution {
public:
vector<vector<int>> findContinuousSequence(int target) {
//初始化窗口
int i = 1, j = 2, s = 3;
//定义容器
vector<vector<int>> res;
//滑动窗口
while(i < j){
//将窗口内的值放入vector中
if(s == target){
vector<int> ans;
for(int k = i; k <= j; k++){
ans.push_back(k);
}
res.push_back(ans);
}
//减少窗口中的值,左边向右移动
if(s >= target){
s -=i;
i++;
}
//增大窗口中的值,右边向右移动
else{
j++;
s +=j;
}
}
return res;
}
};
5.有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
1.左括号必须用相同类型的右括号闭合。
2.左括号必须以正确的顺序闭合。
思路:使用栈解决
1.可以从左到右遍历字符串 s,可以得到最后的左括号先闭合(同类型),然后到前一个左括号闭合,符号栈的特点——后进先出。
2.所以遍历时可以遇到左括号就让其同类型的右括号进栈,然后遇到右括号,就看与栈顶元素是否相等就可以了。
1.stack<char>stk; // 创建栈stk
2.empty()函数:如果栈为空则返回1,若不为空则返回0
3.for(char ch : s) 对数组(或容器类,如vector和array)的每个元素执行相同的操作,此外string类也支持这种对字符的遍历循环操作。ch最初表示数组s的第一个元素,显示第一个元素后,不断执行循环,而ch依次表示数组的其他元素。
4.if(pairs.count(ch) 对ch进行计数,如果为0不执行,不为0则执行语句。
5.pop()出栈,push(ch)入栈。
方法二:栈
class Solution {
public:
bool isValid(string s) {
if(s.size() % 2 == 1) return false;
stack<char> st;
for(int i = 0; i < s.size(); i++){
if(st.empty() && (s[i] == ')' || s[i] == '}' || s[i] == ']')) return false;
if(s[i] == '(' || s[i] == '{' || s[i] == '[') st.push(s[i]);
else if(s[i] == ')' && st.top() == '(' ) st.pop();
else if(s[i] == '}' && st.top() == '{' ) st.pop();
else if(s[i] == ']' && st.top() == '[' ) st.pop();
else return false;
}
if(!st.empty()) return false;
return true;
}
};
方法三:栈
class Solution {
public:
bool isValid(string s) {
if (s.size() % 2 != 0) return false; // 如果s的长度为奇数,一定不符合要求
stack<char> st;
for (int i = 0; i < s.size(); i++) {
if (s[i] == '(') st.push(')');
else if (s[i] == '{') st.push('}');
else if (s[i] == '[') st.push(']');
// 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
// 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
else if (st.empty() || st.top() != s[i]) return false;
else st.pop(); // st.top() 与 s[i]相等,栈弹出元素
}
// 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
return st.empty();
}
};
6.合并两个有序链表
1.迭代:当 list1ist2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里,当一个节点被添加到结果里之后,将对应链表中的节点向后移一位。
2.递归:如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。
7.删除有序数组中的重复项
给你一个 升序排列 的数组 nums
,请你原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
8.移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
方法:使用双指针,两个指针初始时分别位于数组的首尾,向中间移动遍历该序列。如果在遍历过程中有相同的值,就将末尾值赋给当前位置的值,然后继续遍历。
9.搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
思路:因为是有序列表,所以采用二分查找
10.最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
贪心算法:由于要求最大子数组的和,并且连续的,我们就可以通过贪心一直获取数据,一旦发现,加上数组元素结果和为正数,说明接下来继续选取数据有可能使得结果和是更大的值,一旦发现,加上数组元素是结果和是负数,那么我们就可以去掉之前区间的和的值,重新选取新区见继续贪心下去;
方法一:动态规划
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int pre = 0, max1 = nums[0];
for(auto& ch : nums){
pre = max(pre + ch, ch);
max1 = max(pre, max1);
}
return max1;
}
};
11.加一
给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。你可以假设除了整数 0 之外,这个整数不会以零开头。
输入:9,9,9
输出:1,0,0,0
12.二进制求和
给你两个二进制字符串,返回它们的和(用二进制表示),输入为 非空 字符串且只包含数字 1
和 0
。
解题思路:二进制求和,满二进一
1.首先让两个字符串等长,若不等长,在短的字符串前补零,否则之后的操作会超出索引。
2.然后从后到前遍历所有的位数,同位相加,这里有一个点,用的是字符相加,利用 ASCII 码,字符在内部都用数字表示,我们不需要知道具体数值,但可知 ‘0’-‘0’ = 0, ‘0’+1=‘1’,以此类推 。字符的加减,大小比较,实际上都是内部数字的加减,大小比较
3.判断相加后的字符,若大于等于字符 ‘2’,下一位需要进一
4.第 0 位数的相加在这里是单独处理的,因为它可能涉及到字符的插入(即是否需要在最前面加一位数 ‘1’
代码:
13.x 的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
14.爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
方法一:动态规划
思路和算法:我们用 f(x) 表示爬到第 x级台阶的方案数,考虑最后一步可能跨了一级台阶,也可能跨了两级台阶,所以我们可以列出如下式子:f(x) = f(x - 1) + f(x - 2)
它意味着爬到第 x 级台阶的方案数是爬到第 x−1 级台阶的方案数和爬到第 x−2 级台阶的方案数的和。很好理解,因为每次只能爬 1 级或 2 级,所以 f(x) 只能从 f(x−1) 和 f(x−2) 转移过来,而这里要统计方案总数,我们就需要对这两项的贡献求和。
以上是动态规划的转移方程,下面我们来讨论边界条件。我们是从第 0 级开始爬的,所以从第 0 级爬到第 0 级我们可以看作只有一种方案,即 f(0)=1;从第 0 级到第 1 级也只有一种方案,即爬一级,f(1)=1。这两个作为边界条件就可以继续向后推导出第 n 级的正确结果。我们不妨写几项来验证一下,根据转移方程得到 f(2)=2,f(3)=3,f(4)=5,……,我们把这些情况都枚举出来,发现计算的结果是正确的。
我们不难通过转移方程和边界条件给出一个时间复杂度和空间复杂度都是 O(n) 的实现,但是由于这里的 f(x) 只和 f(x−1) 与 f(x−2) 有关,所以我们可以用「滚动数组思想」把空间复杂度优化成 O(1)。下面的代码中给出的就是这种实现。
代码
15.合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n
思路:比较两个数组中的值,每次取两者之中的较大者放进 nums1 的最后面。
代码:
16.树的遍历
二叉树的中序遍历
给定一个二叉树的根节点 root
,返回 它的 中序 遍历 。
方法一:递归
思路与算法
首先我们需要了解什么是二叉树的中序遍历:按照访问左子树——根节点——右子树的方式遍历这棵树,而在访问左子树或者右子树的时候我们按照同样的方式遍历,直到遍历完整棵树。因此整个遍历过程天然具有递归的性质,我们可以直接用递归函数来模拟这一过程。
定义 inorder(root) 表示当前遍历到 root 节点的答案,那么按照定义,我们只要递归调用 inorder(root.left) 来遍历 root 节点的左子树,然后将 root 节点的值加入答案,再递归调用inorder(root.right) 来遍历 root 节点的右子树即可,递归终止的条件为碰到空节点。
代码:
class Solution {
public:
void inorder(TreeNode* root, vector<int>& res) {
if (!root) {
return;
}
inorder(root->left, res);
res.push_back(root->val);
inorder(root->right, res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
inorder(root, res);
return res;
}
};
方法二:迭代
思路与算法
方法一的递归函数我们也可以用迭代的方式实现,两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其他都相同,具体实现可以看下面的代码。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> stk;
while (root != nullptr || !stk.empty()) {
while (root != nullptr) {
stk.push(root);
root = root->left;
}
root = stk.top();
stk.pop();
res.push_back(root->val);
root = root->right;
}
return res;
}
};
方法三:Morris 中序遍历
思路与算法
Morris 遍历算法整体步骤如下(假设当前遍历到的节点为 x):
- 如果 x 无左孩子,先将 x 的值加入答案数组,再访问 x 的右孩子,即 x = x.right。
- 如果 x 有左孩子,则找到 x 左子树上最右的节点(即左子树中序遍历的最后一个节点,x 在中序遍历中的前驱节点),我们记为 predecessor。根据 predecessor 的右孩子是否为空,进行如下操作。
- 如果 predecessor 的右孩子为空,则将其右孩子指向 x,然后访问 x 的左孩子,即 x = x.left。
- 如果 predecessor 的右孩子不为空,则此时其右孩子指向 x,说明我们已经遍历完 x 的左子树,我们将 predecessor 的右孩子置空,将 x 的值加入答案数组,然后访问 x 的右孩子,即 x = x.right。
- 重复上述操作,直至访问完整棵树。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
TreeNode *predecessor = nullptr;
while (root != nullptr) {
if (root->left != nullptr) {
// predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止
predecessor = root->left;
while (predecessor->right != nullptr && predecessor->right != root) {
predecessor = predecessor->right;
}
// 让 predecessor 的右指针指向 root,继续遍历左子树
if (predecessor->right == nullptr) {
predecessor->right = root;
root = root->left;
}
// 说明左子树已经访问完了,我们需要断开链接
else {
res.push_back(root->val);
predecessor->right = nullptr;
root = root->right;
}
}
// 如果没有左孩子,则直接访问右孩子
else {
res.push_back(root->val);
root = root->right;
}
}
return res;
}
};
二叉树的前序遍历
给你二叉树的根节点 root
,返回它节点值的 前序 遍历。
方法一:递归
class Solution {
public:
void preorder(TreeNode *root, vector<int> &res) {
if (root == nullptr) {
return;
}
res.push_back(root->val);
preorder(root->left, res);
preorder(root->right, res);
}
vector<int> preorderTraversal(TreeNode *root) {
vector<int> res;
preorder(root, res);
return res;
}
};
方法二:迭代
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if (root == nullptr) {
return res;
}
stack<TreeNode*> stk;
TreeNode* node = root;
while (!stk.empty() || node != nullptr) {
while (node != nullptr) {
res.emplace_back(node->val);
stk.emplace(node);
node = node->left;
}
node = stk.top();
stk.pop();
node = node->right;
}
return res;
}
};
方法三:Morris 遍历
Morris 遍历的核心思想是利用树的大量空闲指针,实现空间开销的极限缩减。其前序遍历规则总结如下:
- 新建临时节点,令该节点为 root;
- 如果当前节点的左子节点为空,将当前节点加入答案,并遍历当前节点的右子节点;
- 如果当前节点的左子节点不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点:
- 如果前驱节点的右子节点为空,将前驱节点的右子节点设置为当前节点。然后将当前节点加入答案,并将前驱节点的右子节点更新为当前节点。当前节点更新为当前节点的左子节点。
- 如果前驱节点的右子节点为当前节点,将它的右子节点重新设为空。当前节点更新为当前节点的右子节点。
- 重复步骤 2 和步骤 3,直到遍历结束。
class Solution {
public:
vector<int> preorderTraversal(TreeNode *root) {
vector<int> res;
if (root == nullptr) {
return res;
}
TreeNode *p1 = root, *p2 = nullptr;
while (p1 != nullptr) {
p2 = p1->left;
if (p2 != nullptr) {
while (p2->right != nullptr && p2->right != p1) {
p2 = p2->right;
}
if (p2->right == nullptr) {
res.emplace_back(p1->val);
p2->right = p1;
p1 = p1->left;
continue;
} else {
p2->right = nullptr;
}
} else {
res.emplace_back(p1->val);
}
p1 = p1->right;
}
return res;
}
};
二叉树的后序遍历
给你一棵二叉树的根节点 root
,返回其节点值的 后序遍历 。
方法一:递归
class Solution {
public:
void postorder(TreeNode *root, vector<int> &res) {
if (root == nullptr) {
return;
}
postorder(root->left, res);
postorder(root->right, res);
res.push_back(root->val);
}
vector<int> postorderTraversal(TreeNode *root) {
vector<int> res;
postorder(root, res);
return res;
}
};
方法二:迭代
class Solution {
public:
vector<int> postorderTraversal(TreeNode *root) {
vector<int> res;
if (root == nullptr) {
return res;
}
stack<TreeNode *> stk;
TreeNode *prev = nullptr;
while (root != nullptr || !stk.empty()) {
while (root != nullptr) {
stk.emplace(root);
root = root->left;
}
root = stk.top();
stk.pop();
if (root->right == nullptr || root->right == prev) {
res.emplace_back(root->val);
prev = root;
root = nullptr;
} else {
stk.emplace(root);
root = root->right;
}
}
return res;
}
};
方法三:Morris 遍历
Morris 遍历的核心思想是利用树的大量空闲指针,实现空间开销的极限缩减。其后序遍历规则总结如下:
- 新建临时节点,令该节点为 root;
- 如果当前节点的左子节点为空,则遍历当前节点的右子节点;
- 如果当前节点的左子节点不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点;
- 如果前驱节点的右子节点为空,将前驱节点的右子节点设置为当前节点,当前节点更新为当前节点的左子节点。
- 如果前驱节点的右子节点为当前节点,将它的右子节点重新设为空。倒序输出从当前节点的左子节点到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右子节点。
- 重复步骤 2 和步骤 3,直到遍历结束。
class Solution {
public:
void addPath(vector<int> &vec, TreeNode *node) {
int count = 0;
while (node != nullptr) {
++count;
vec.emplace_back(node->val);
node = node->right;
}
reverse(vec.end() - count, vec.end());
}
vector<int> postorderTraversal(TreeNode *root) {
vector<int> res;
if (root == nullptr) {
return res;
}
TreeNode *p1 = root, *p2 = nullptr;
while (p1 != nullptr) {
p2 = p1->left;
if (p2 != nullptr) {
while (p2->right != nullptr && p2->right != p1) {
p2 = p2->right;
}
if (p2->right == nullptr) {
p2->right = p1;
p1 = p1->left;
continue;
} else {
p2->right = nullptr;
addPath(res, p1->left);
}
}
p1 = p1->right;
}
addPath(res, root);
return res;
}
};
17. 相同的树
给你两棵二叉树的根节点 p
和 q
,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
方法一:深度优先搜索
如果两个二叉树都为空,则两个二叉树相同。如果两个二叉树中有且只有一个为空,则两个二叉树一定不相同。如果两个二叉树都不为空,那么首先判断它们的根节点的值是否相同,若不相同则两个二叉树一定不同,若相同,再分别判断两个二叉树的左子树是否相同以及右子树是否相同。这是一个递归的过程,因此可以使用深度优先搜索,递归地判断两个二叉树是否相同。
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if (p == nullptr && q == nullptr) {
return true;
} else if (p == nullptr || q == nullptr) {
return false;
} else if (p->val != q->val) {
return false;
} else {
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
}
};
18.对称二叉树
给你一个二叉树的根节点 root
, 检查它是否轴对称。
方法一:递归
class Solution {
public:
bool check(TreeNode *p, TreeNode *q){
if(!p && !q) return true;
if(!p || !q) return false;
return p->val == q->val && check(p->left, q->right) && check(p->right, q->left);
}
bool isSymmetric(TreeNode* root) {
return check(root,root);
}
};
19.将有序数组转换为二叉搜索树
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 的二叉树。
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
return helper(nums, 0, nums.size() - 1);
}
TreeNode* helper(vector<int>& nums, int left, int right) {
if (left > right) {
return nullptr;
}
// 总是选择中间位置左边的数字作为根节点
int mid = (left + right) / 2;
TreeNode* root = new TreeNode(nums[mid]);
root->left = helper(nums, left, mid - 1);
root->right = helper(nums, mid + 1, right);
return root;
}
};
20.平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
方法一:自顶向下的递归
class Solution {
public:
int height(TreeNode *root){
if(root == nullptr){
return 0;
}else{
return max(height(root->left), height(root->right)) + 1;
}
}
bool isBalanced(TreeNode* root) {
if(root == nullptr){
return true;
}else{
return abs(height(root->left) - height(root->right)) <= 1 && isBalanced(root->left) && isBalanced(root->right);
}
}
};
方法二:自底向上的递归
class Solution {
public:
int height(TreeNode *root){
if(root == nullptr) return 0;
int LeftHeight = height(root->left);
int RightHeight = height(root->right);
if(LeftHeight == -1 || RightHeight == -1 || abs(LeftHeight - RightHeight) > 1) {
return -1;
}else{
return max(height(root->left), height(root->right)) + 1;
}
}
bool isBalanced(TreeNode* root) {
return height(root) >= 0;
}
};
21. 二叉树的最小深度
给定一个二叉树,找出其最小深度。最小深度是从根节点到最近叶子节点的最短路径上的节点数量。说明:叶子节点是指没有子节点的节点。
方法一:深度优先搜索
class Solution {
public:
int minDepth(struct TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return 1;
if (root->left == NULL || root->right == NULL)
return minDepth(root->right ? root->right : root->left) + 1;
return fmin(minDepth(root->left), minDepth(root->right)) + 1;
}
};
方法二:广度优先搜索
class Solution {
public:
int minDepth(TreeNode *root) {
if (root == nullptr) {
return 0;
}
queue<pair<TreeNode *, int> > que;
que.emplace(root, 1);
while (!que.empty()) {
TreeNode *node = que.front().first;
int depth = que.front().second;
que.pop();
if (node->left == nullptr && node->right == nullptr) {
return depth;
}
if (node->left != nullptr) {
que.emplace(node->left, depth + 1);
}
if (node->right != nullptr) {
que.emplace(node->right, depth + 1);
}
}
return 0;
}
};
22.路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
方法一:广度优先搜索
class Solution {
public:
bool hasPathSum(TreeNode *root, int sum) {
if (root == nullptr) {
return false;
}
queue<TreeNode *> que_node;
queue<int> que_val;
que_node.push(root);
que_val.push(root->val);
while (!que_node.empty()) {
TreeNode *now = que_node.front();
int temp = que_val.front();
que_node.pop();
que_val.pop();
if (now->left == nullptr && now->right == nullptr) {
if (temp == sum) {
return true;
}
continue;
}
if (now->left != nullptr) {
que_node.push(now->left);
que_val.push(now->left->val + temp);
}
if (now->right != nullptr) {
que_node.push(now->right);
que_val.push(now->right->val + temp);
}
}
return false;
}
};
方法二:递归
class Solution {
public:
bool hasPathSum(TreeNode *root, int sum) {
if (root == nullptr) {
return false;
}
if (root->left == nullptr && root->right == nullptr) {
return sum == root->val;
}
return hasPathSum(root->left, sum - root->val) ||
hasPathSum(root->right, sum - root->val);
}
};
23.杨辉三角
给定一个非负整数 numRows
,生成「杨辉三角」的前 numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> ret(numRows);
for (int i = 0; i < numRows; i++) {
ret[i].resize(i + 1);
ret[i][0] = ret[i][i] = 1;
for (int j = 1; j < i; j++) {
ret[i][j] = ret[i - 1][j -1] + ret[i - 1][j];
}
}
return ret;
}
};
给定一个非负索引 rowIndex
,返回「杨辉三角」的第 rowIndex
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
方法一 : 公式
1LL是long long int 长长整形
class Solution {
public:
vector<int> getRow(int rowIndex) {
vector<int> row(rowIndex + 1);
row[0] = 1;
for (int i = 1; i <= rowIndex; ++i) {
row[i] = 1LL * row[i - 1] * (rowIndex - i + 1) / i;
}
return row;
}
};
方法二:递推
class Solution {
public:
vector<int> getRow(int rowIndex) {
vector<vector<int>> C(rowIndex + 1);
for (int i = 0; i <= rowIndex; ++i) {
C[i].resize(i + 1);
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; ++j) {
C[i][j] = C[i - 1][j - 1] + C[i - 1][j];
}
}
return C[rowIndex];
}
};
优化
注意到对第 i+1i+1 行的计算仅用到了第 ii 行的数据,因此可以使用滚动数组的思想优化空间复杂度。
class Solution {
public:
vector<int> getRow(int rowIndex) {
vector<int> pre, cur;
for (int i = 0; i <= rowIndex; ++i) {
cur.resize(i + 1);
cur[0] = cur[i] = 1;
for (int j = 1; j < i; ++j) {
cur[j] = pre[j - 1] + pre[j];
}
pre = cur;
}
return pre;
}
};
进一步优化
当前行第 ii 项的计算只与上一行第 i-1 项及第 i 项有关。因此我们可以倒着计算当前行,这样计算到第 i 项时,第 i−1 项仍然是上一行的值。
class Solution {
public:
vector<int> getRow(int rowIndex) {
vector<int> row(rowIndex + 1);
row[0] = 1;
for (int i = 1; i <= rowIndex; ++i) {
for (int j = i; j > 0; --j) {
row[j] += row[j - 1];
}
}
return row;
}
};
24.买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
方法一:一次遍历
思路:用一个变量记录一个历史最低价格 minprice,我们就可以假设自己的股票是在那天买的。那么我们在第 i 天卖出股票能得到的利润就是 prices[i] - minprice。每天将股票卖出,比较利润,遍历一次即可
class Solution {
public:
int maxProfit(vector<int>& prices) {
int inf = 1e9;
int minprice = inf, maxprofit = 0;
for (int price: prices) {
maxprofit = max(maxprofit, price - minprice);
minprice = min(price, minprice);
}
return maxprofit;
}
};
方法二:单调栈
思路:这个题本质就是要求某个数与其右边最大的数的差值
首先讲下维护单调栈的 具体思路:
- 在 pricesprices 数组的末尾加上一个 哨兵(也就是一个很小的元素,这里设为 0)),就相当于作为股市收盘的标记
- 假如栈空或者入栈元素大于栈顶元素,直接入栈
- 假如入栈元素小于栈顶元素则循环弹栈,直到入栈元素大于栈顶元素或者栈空
- 在每次弹出的时候,我们拿他与买入的值(也就是栈底)做差,维护一个最大值。
(灰色标记为扫描过的)
①:第一步,栈空,扫描的是 7,我们直接入栈。
②:第二步,入栈元素为 1,他比栈顶元素小,为了维护这个单调栈,我们把7弹出,又因为他即是栈底又是栈顶所以不需要更新我们的最大值,又因为弹出之后为空,我们将1直接入栈。
③:第三步,入栈元素为 5,他比栈顶元素大,我们直接入栈
④:第四步,入栈元素为 3,他比栈顶元素 5 小,我们直接将 5 弹栈,并拿他减去栈底元素 1 (这就是最重要的,模拟了买卖,因为 5 遇上了比它小的 3,因此即使后面遇到更大的元素 C,但是存在 C - 1 > 5 - 1 ,此时5的最高利润已经确定了,因此它已经没用了,计算之后弹出它
⑤:第五步,入栈元素为 6 ,比栈顶元素大,入栈。
⑥:第六步,入栈元素为 4 ,比栈顶元素 6 小,根据我们刚刚的理论,在遇上 4 之后,6 的最高利润已经确定了, 所以我们弹出 6,并与栈底(也就是我们买入的价值)做差,并与我们之前维护的最大值进行比较,然后更新。
⑦:第七步,现在 哨兵的作用就非常清楚啦,假如没有哨兵,我们单调栈中还有残留的元素没有进行判断(比如 prices 数组单调增的情况下,不加哨兵会出现 max=0 的情况),因此 哨兵的作用就是确保单调栈中的每个元素都被进行判定。因此最后的图像应该是这样:
代码
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ans = 0;
vector<int> St;
prices.emplace_back(-1); \\ 哨兵
for (int i = 0; i < prices.size(); ++ i){
while (!St.empty() && St.back() > prices[i]){ \\ 维护单调栈
ans = std::max(ans, St.back() - St.front()); \\ 维护最大值
St.pop_back();
}
St.emplace_back(prices[i]);
}
return ans;
}
};
25.验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。说明:本题中,我们将空字符串定义为有效的回文串。
思路:我们直接在原字符串 ss 上使用双指针。在移动任意一个指针时,需要不断地向另一指针的方向移动,直到遇到一个字母或数字字符,或者两指针重合为止。
class Solution {
public:
bool isPalindrome(string s) {
int n = s.size();
int left = 0, right = n - 1;
while(left < right){
while(left < right && !isalnum(s[left])) left++;
while(left < right && !isalnum(s[right])) right--;
if(left < right){
if(tolower(s[left]) != tolower(s[right])){
return false;
}
left++;
right--;
}
}
return true;
}
};
isalnum():判断一个字符是否是数字或字母
tolower():将大写字母转换为小写字母
26.只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。说明:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
异或运算有以下性质。
- 相同为0, 不同为1
- 任何数和 0 做异或运算,结果仍然是原来的数,即即 a ⊕ 0 = a。
- 任何数和其自身做异或运算,结果是 0,即 a ⊕ a = 0。
- 异或运算满足交换律和结合律,即 a ⊕ b ⊕ a = b ⊕ a ⊕ a =b ⊕ (a ⊕ a) = b ⊕ 0 = b。
class Solution {
public:
int singleNumber(vector<int>& nums) {
int res = 0;
for(auto n : nums) res ^= n;
return res;
}
};
27.环形链表
给你一个链表的头节点 head ,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。如果链表中存在环 ,则返回 true 。 否则,返回 false 。
方法一:哈希表
遍历链表,用哈希表记录遍历过的节点。遍历过程中,如果发现某个节点已经存在于哈希表中了,就说明这个节点遍历过了,也就是说有环。一旦遍历到了“next为空”的某个节点,就说明这个节点是链表的最后一个节点,也就是说无环。
class Solution {
public:
bool hasCycle(ListNode *head) {
unordered_set<ListNode*> seen;
while (head != nullptr) {
if (seen.count(head)) {
return true;
}
seen.insert(head);
head = head->next;
}
return false;
}
};
方法二:快慢指针
我们定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
注意:若有环,则快慢指针一定会相遇。因为快指针一定比慢指针提前进入到环中,等慢指针也进入环中后,快指针一定会追上满指针(因为速度是慢指针的两倍),并且一定不会不相遇而直接跳过去(慢指针移动前的旧位置和移动后的新位置共2个节点,快指针一次前进2个节点,必定踩上一个)
class Solution {
public:
bool hasCycle(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return false;
}
ListNode* slow = head;
ListNode* fast = head->next;
while (slow != fast) {
if (fast == nullptr || fast->next == nullptr) {
return false;
}
slow = slow->next;
fast = fast->next->next;
}
return true;
}
};
28.相交链表
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
方法一:哈希集合
思路和算法
判断两个链表是否相交,可以使用哈希集合存储链表节点。
首先遍历链表 headA,并将链表 headA 中的每个节点加入哈希集合中。然后遍历链表 headB,对于遍历到的每个节点,判断该节点是否在哈希集合中:
- 如果当前节点不在哈希集合中,则继续遍历下一个节点;
- 如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表 headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。
- 如果链表 headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
unordered_set<ListNode*> map;
ListNode* temp = headA;
//将headA的节点存入哈希表中
while(temp != nullptr){
map.insert(temp);
temp = temp->next;
}
//查找heaB中是否有相同的节点
temp = headB;
while(temp != nullptr){
if(map.count(temp)) return temp;
temp = temp->next;
}
return nullptr;
}
};
方法二:双指针
思路和算法
只有当链表 headA 和 headB 都不为空时,两个链表才可能相交。因此首先判断链表 headA 和 headB 是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 null。
当链表 headA 和 headB 都不为空时,创建两个指针 a 和 b,初始时分别指向两个链表的头节点 headA 和 headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:
每步操作需要同时更新指针a 和 b。
- 如果指针 a 不为空,则将指针 a 移到下一个节点;如果指针 b 不为空,则将指针 b 移到下一个节点。
- 如果指针 a 为空,则将指针 a 移到链表 headB 的头节点;如果指针 b 为空,则将指针 b 移到链表 headA 的头节点。
- 当指针 a 和 b 指向同一个节点或者都为空时,返回它们指向的节点或者 null。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == nullptr || headB == nullptr) {
return nullptr;
}
ListNode *pA = headA, *pB = headB;
while (pA != pB) {
pA = pA == nullptr ? headB : pA->next;
pB = pB == nullptr ? headA : pB->next;
}
return pA;
}
};
29.Excel表列名称
给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。
例如:
A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
...
和正常 0~25 的 26 进制相比,本质上就是每一位多加了 1。假设 A == 0,B == 1,那么 AB = 26 * 0 + 1 * 1,而现在 AB = 26 * (0 + 1) + 1 * (1 + 1),所以只要在处理每一位的时候减 1,就可以按照正常的 26 进制来处理
class Solution {
public:
string convertToTitle(int columnNumber) {
string ans;
while (columnNumber > 0) {
--columnNumber;
ans += columnNumber % 26 + 'A';
columnNumber /= 26;
}
reverse(ans.begin(), ans.end());
return ans;
}
};
30. 多数元素
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。
方法一:哈希表
- 我们使用哈希映射(HashMap)来存储每个元素以及出现的次数。对于哈希映射中的每个键值对,键表示一个元素,值表示该元素出现的次数。
- 我们用一个循环遍历数组 nums 并将数组中的每个元素加入哈希映射中。在这之后,我们遍历哈希映射中的所有键值对,返回值最大的键。我们同样也可以在遍历数组 nums 时候使用打擂台的方法,维护最大的值,这样省去了最后对哈希映射的遍历。
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int, int> counts;
int majority = 0, cnt = 0;
for (int num: nums) {
++counts[num];
if (counts[num] > cnt) {
majority = num;
cnt = counts[num];
}
}
return majority;
}
};
方法二:排序
class Solution {
public:
int majorityElement(vector<int>& nums) {
sort(nums.begin(), nums.end());
return nums[nums.size() / 2];
}
};
方法三:随机化
class Solution {
public:
int majorityElement(vector<int>& nums) {
while(1){
int n = nums[rand() % nums.size()];
int c = 0;
for(int num : nums){
if(n == num) c++;
if(c > nums.size() >> 1) return n;
}
}
return -1;
}
};
方法四:Boyer-Moore 投票算法
思路:不妨假设整个数组的众数记做a,则最初的数组中a的数量大于其余所有数。当采用count计数的时候有两种情况:
1)假设candidate等于a,则当count从1变为0的过程,此区间内a的数量等于其余数的数量,因此以count=0为分界线,数组右端部分的众数仍然为a
2)假设candidate不等于a,则当count从1变为0的过程, 此区间内a的数量小于等于其余数的数量,因此以count=0为分界线,数组右端部分的众数仍然为a
因此,以count=0可以将整个原始数组分为若干部分,count=0右端部分的数组中的众数永远是a,最终必然会筛选出a
1.
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate = -1;
int count = 0;
for (int num : nums) {
if (num == candidate)
++count;
else if (--count < 0) {
candidate = num;
count = 1;
}
}
return candidate;
}
};
2.
class Solution {
public:
int majorityElement(vector<int>& nums) {
int zs = 0, count = 0;
for(auto ch : nums){
if(count == 0) zs = ch;
count += ch == zs ? 1 : -1;
}
// 验证 x 是否为众数
for(int ch : nums)
if(ch == zs) count++;
return count > nums.size() / 2 ? zs : 0; // 当无众数时返回 0
}
};
31 .Excel 表列序号
给你一个字符串 columnTitle ,表示 Excel 表格中的列名称。返回 该列名称对应的列序号 。
例如:
A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
...
思路:本质是26进制问题
class Solution {
public:
int titleToNumber(string s) {
int num = 0;
for(int i = 0; i < s.size() ; i++ ){
num = num * 26 + (s[i] - 'A' + 1);
}
return num;
}
};
32. 颠倒二进制位
颠倒给定的 32 位无符号整数的二进制位。
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
int num = 0;
for(int i = 0; i < 32; i++){
num = ( num << 1) + (n % 2);
n = n >> 1;
}
return num;
}
};
33.位1的个数
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。
class Solution {
public:
int hammingWeight(uint32_t n) {
int num=0;
while(n>0)
{
if(n&1==1)
{
num++;
}
n=n>>1;
}
return num;
}
};
34.快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
方法一:用哈希表检测循环
思路:如果不是快乐数,在循环的过程中就会出现重复的值,(不会一直无限增大,99->162,999->243),因此可以用unordered_set来存储中间值,出现1就说明是快乐数。一旦出现与unordered_set中相同的值,说明不是快乐数。
class Solution {
int get(int n){
int sum = 0;
while(n){
sum += (n % 10) * (n % 10);
n = n /10;
}
return sum;
}
public:
bool isHappy(int n) {
unordered_set<int> map;
int num = n;
while(1){
num = get(num);
if(num == 1) return true;
if(map.find(num) != map.end()) return false;
map.insert(num);
}
}
};
方法二:快慢指针法
思路:设置快慢指针,快指针每次走两步,慢指针每次走一步
class Solution {
int get(int n){
int sum = 0;
while(n){
sum += (n % 10) * (n % 10);
n = n /10;
}
return sum;
}
public:
bool isHappy(int n) {
int slow = get(n);
int fast = get(slow);
while(slow != fast){
slow = get(slow);
fast = get(fast);
fast = get(fast);
}
if(slow == 1) return true;
else return false;
}
};
35.移除链表元素
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
方法一:递归
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(head == nullptr) return head;
head->next = removeElements(head->next, val);
return head->val == val ? head->next : head;
}
};
方法二:迭代
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
方法一:
struct ListNode *phead = new ListNode(0, head);
struct ListNode *temp = phead;
while(temp->next != nullptr){
if(temp->next->val == val){
temp->next = temp->next->next;
}else{
temp =temp->next;
}
}
return phead->next;
方法二:
if(head->val == val) return head->next;
ListNode *pre = head, *cur = head->next;
while(cur != nullptr && cur->val != val){
pre = cur;
cur = cur->next;
}
if(cur != nullptr) pre->next = cur->next;
return head;
}
};
36.同构字符串
给定两个字符串 s 和 t ,判断它们是否是同构的。如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
方法一:哈希表
class Solution {
public:
bool isIsomorphic(string s, string t) {
unordered_map<char, char> s1;
unordered_map<char, char> t1;
for(int i = 0; i < s.size(); ++i){
char x = s[i], y = t[i];
if(s1.count(x) && s1[x] != y || t1.count(y) && t1[y] != x) return false;
s1[x] = y;
t1[y] = x;
}
return true;
}
};
37.反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
方法一:迭代
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *phead = head;
ListNode *pre = nullptr;
while(phead){
ListNode *next = phead->next;
phead->next = pre;
pre = phead;
phead = next;
}
return pre;
}
};
方法二:递归
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(!head || !head->next) return head;
ListNode *next = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return next;
}
};
38.存在重复元素
给你一个整数数组 nums
。如果任一值在数组中出现 至少两次 ,返回 true
;如果数组中每个元素互不相同,返回 false
。
方法一:排序
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size() - 1; i++){
if(nums[i] == nums[i + 1]) return true;
}
return false;
}
};
方法二:哈希表
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
//法一
unordered_map<int, int> s;
for(int num : nums){
++s[num];
if(s[num] > 1) return true;
}
return false;
//法二
unordered_set<int> s;
for(int num : nums) {
if( s.find(num) != s.end()) return true;
s.insert(num);
}
return false;
}
};
存在重复元素 II
给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。
方法一:哈希表
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_map<int, int> s;
for(int i = 0; i < nums.size(); i++){
int num = nums[i];
if(s.count(num) && i - s[num] <= k) return true;
s[num] = i;
}
return false;
}
};
方法二:滑动窗口
思路:考虑数组 nums 中的每个长度不超过 k + 1 的滑动窗口,同一个滑动窗口中的任意两个下标差的绝对值不超过 k 。如果存在一个滑动窗口,其中有重复元素,则存在两个不同的下标 i 和 j 满足 nums[i] = nums[j] 且 ∣i−j∣≤ k。如果所有滑动窗口中都没有重复元素,则不存在符合要求的下标。因此,只要遍历每个滑动窗口,判断滑动窗口中是否有重复元素即可。
如果一个滑动窗口的结束下标是 i,则该滑动窗口的开始下标是 max(0 ,i − k) 。可以使用哈希集合存储滑动窗口中的元素。从左到右遍历数组 nums ,当遍历到下标 i 时,具体操作如下:
- 如果 i > k ,则下标 i - k - 1 处的元素被移出滑动窗口,因此将 nums[i− k − 1] 从哈希集合中删除;
- 判断 nums[i] 是否在哈希集合中,如果在哈希集合中则在同一个滑动窗口中有重复元素,返回 true ,如果不在哈希集合中则将其加入哈希集合。
当遍历结束时,如果所有滑动窗口中都没有重复元素,返回 false 。
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_set<int> s;
for(int i = 0; i < nums.size(); i++){
if(i > k) s.erase(nums[i - k - 1]);
if(s.count(nums[i])) return true;
s.emplace(nums[i]);
}
return false;
}
};
39.用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
- void push(int x) 将元素 x 压入栈顶。
- int pop() 移除并返回栈顶元素。
- int top() 返回栈顶元素。
- boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
注意:
- 你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
- 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
方法一:两个队列
思路:
为了满足栈的特性,即最后入栈的元素最先出栈,在使用队列实现栈时,应满足队列前端的元素是最后入栈的元素。可以使用两个队列实现栈的操作,其中 q1 用于存储栈内的元素,q2 作为入栈操作的辅助队列。
- 入栈操作时,首先将元素入队到q2 ,然后将 q1的全部元素依次出队并入队到 q2,此时 q2 的前端的元素即为新入栈的元素,再将 q1 和 q2互换,则 q1 的元素即为栈内的元素,q1 的前端和后端分别对应栈顶和栈底。
- 由于每次入栈操作都确保 q1 的前端元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以简单实现。
- 出栈操作只需要移除 q1的前端元素并返回即可,
- 获得栈顶元素操作只需要获得 q1 的前端元素并返回即可(不移除元素)。
- 由于 q1 用于存储栈内的元素,判断栈是否为空时,只需要判断q1 , 是否为空即可。
class MyStack {
public:
//创建两个队列
queue<int> q1;
queue<int> q2;
//在这里初始化你的数据结构
MyStack() {
}
//q1相当于栈,q2起辅助作用
//用队列实现入栈
void push(int x) {
q2.push(x); //入队列q2;
while(!q1.empty()){
q2.push(q1.front()); //将q1中数据入到q2中,模拟先进后出,后进先出
q1.pop(); // q1出队列
}
swap(q1, q2); //交换q1, q2
}
//用队列实现出栈
int pop() {
int r = q1.front();
q1.pop();
return r;
}
//用队列实现返回栈顶元素
int top() {
int r = q1.front();
return r;
}
//用队列实现判断栈是否为空
bool empty() {
return q1.empty();
}
};
方法二:一个队列
思路:
- 入栈操作时,首先获得入栈前的元素个数 n,然后将元素入队到队列,再将队列中的前 n 个元素(即除了新入栈的元素之外的全部元素)依次出队并入队到队列,此时队列的前端的元素即为新入栈的元素,且队列的前端和后端分别对应栈顶和栈底。
- 由于每次入栈操作都确保队列的前端元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以简单实现。
- 出栈操作只需要移除队列的前端元素并返回即可,
- 获得栈顶元素操作只需要获得队列的前端元素并返回即可(不移除元素)。
- 由于队列用于存储栈内的元素,判断栈是否为空时,只需要判断队列是否为空即可。
class MyStack {
public:
//初始化一个队列
queue<int> q;
//在这里初始化你的数据结构
MyStack() {
}
//用队列实现入栈
void push(int x) {
int n = q.size();
q.push(x);
//每次入栈都会重新入队,保证先进后出,后进先出
for(int i = 0; i < n; i++){
q.push(q.front());
q.pop();
}
}
//用队列实现出栈
int pop() {
int r = q.front();
q.pop();
return r;
}
//用队列实现返回栈顶元素
int top() {
int r = q.front();
return r;
}
bool empty() {
return q.empty();
}
};
40.用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
- void push(int x) 将元素 x 推到队列的末尾
- int pop() 从队列的开头移除并返回元素
- int peek() 返回队列开头的元素
- boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
思路:用两个栈来实现,一个栈负责入栈,一个栈负责出栈。
class MyQueue {
private:
//实例化栈
stack<int> ins, outs;
//将ins中的数据转移到outs中,保证先进先出
void inout(){
while(!ins.empty()){
outs.push(ins.top());
ins.pop();
}
}
public:
//在这里初始化你的数据结构
MyQueue() {
}
//用栈实现队列的入队
void push(int x) {
ins.push(x);
}
//用栈实现队列的出队
int pop() {
if(outs.empty()){
inout();
}
int x = outs.top();
outs.pop();
return x;
}
//用栈实现返回队列开头的元素
int peek() {
if(outs.empty()){
inout();
}
int x = outs.top();
return x;
}
//判断队列是否为空
bool empty() {
return ins.empty() && outs.empty();
}
};
40.翻转二叉树
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点
方法一: 基于递归的方法
class Solution
{
public:
// 基于递归的方法
TreeNode *invertTree(TreeNode *root)
{
if(root == nullptr) return root;
swap(root->left, root->right);
invertTree(root->left);
invertTree(root->right);
return root;
}
};
方法二:基于栈的遍历方法
class Solution
{
public:
// 基于栈的后续遍历方法
TreeNode *invertTree(TreeNode *root)
{
// 建立一个栈用来缓存待翻转的二叉树
stack<TreeNode *> tree_cache;
// 如果二叉树的根节点不为空
if(root!=nullptr)
{
// 二叉树的根节点入栈
tree_cache.push(root);
}
// 如果栈不空,则持续遍历栈
while(!tree_cache.empty())
{
// 保存栈顶节点
TreeNode *top_node=tree_cache.top();
// 如果栈顶节点不空
if(top_node!=nullptr)
{
// 栈顶节点暂时出栈
tree_cache.pop();
// 如果栈顶节点的左节点不空
if(top_node->left!=nullptr)
{
// 栈顶节点的左节点进栈
tree_cache.push(top_node->left);
}
// 如果栈顶节点的右节点不空
if(top_node->right!=nullptr)
{
// 栈顶节点的右节点进栈
tree_cache.push(top_node->right);
}
// 将一开始出栈的栈顶节点再入栈
tree_cache.push(top_node);
// 将一个空节点入栈
// 这个空节点为了标识当前需要翻转的根节点
tree_cache.push(nullptr);
}
else
{
// 如果栈顶节点为空
// 这个空节点就是之前入栈的用来标识当前需要翻转的根节点的空节点
tree_cache.pop();
// #include<algorithm>
// C++中swap:交换相同类型的两个变量内容
// 这里交换栈顶节点的左右节点
swap(tree_cache.top()->left,tree_cache.top()->right);
// 将已经交换过的栈顶节点出栈
tree_cache.pop();
}
}
// 返回翻转后的二叉树
return root;
}
};
方法三:基于队列的层序遍历方法
class Solution
{
public:
// 基于队列的层序遍历方法
TreeNode *invertTree(TreeNode *root)
{
// 创建一个队列,用于层序遍历
queue<TreeNode *> tree_cache;
// 如果待翻转的二叉树根节点不为空
if(root!=nullptr)
{
// 二叉树根节点入队
tree_cache.push(root);
}
// 如果队列不空
while(!tree_cache.empty())
{
// 缓存队头节点
TreeNode *front_node;
// 循环遍历队列
for(int i=0;i<tree_cache.size();i++)
{
// 缓存队头节点
front_node=tree_cache.front();
// 队头节点出队
tree_cache.pop();
// #include<algorithm>
// C++中swap:交换相同类型的两个变量内容
// 交换队头节点的左右两个节点
swap(front_node->left,front_node->right);
// 如果左节点不空
if(front_node->left!=nullptr)
{
// 左节点进队
tree_cache.push(front_node->left);
}
// 如果右节点不空
if(front_node->right!=nullptr)
{
// 右节点进队
tree_cache.push(front_node->right);
}
}
}
// 返回翻转后的二叉树
return root;
}
};
41.汇总区间
给定一个 无重复元素 的 有序 整数数组 nums 。返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于 nums 的数字 x 。
列表中的每个区间范围 [a,b] 应该按如下格式输出:
- "a->b" ,如果 a != b
- "a" ,如果 a == b
注释:12345这样连续的子段就用1->5表示,如果中间断开了比如123567这样就表示为1->3,5->7
class Solution {
public:
vector<string> summaryRanges(vector<int>& nums) {
vector<string> s;
int a = 0, b = nums.size();
while(a < b){
int low = a;
a++;
while(a < b && nums[a] == nums[a - 1] + 1){
a++;
}
int hig = a - 1;
string temp = to_string(nums[low]);
if(low < hig){
temp.append("->");
temp.append(to_string(nums[hig]));
}
s.push_back(move(temp));
}
return s;
}
};
append:在字符串的末尾添加字符串
to_string:转换成字符串
move: 将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。
42.2 的幂
给你一个整数 n,请你判断该整数是否是 2 的幂次方。如果是,返回 true ;否则,返回 false 。如果存在一个整数 x 使得 n == 2x ,则认为 n 是 2 的幂次方。
思路:
一个数 n 是 2 的幂,当且仅当 n 是正整数,并且 n 的二进制表示中仅包含 1 个 1。
两种常见的与「二进制表示中最低位」相关的位运算技巧。
- n & (n - 1),该位运算技巧可以直接将 n 二进制表示的最低位 1 移除
- n & (-n),该位运算技巧可以直接获取 n 二进制表示的最低位的 1
class Solution {
public:
bool isPowerOfTwo(int n) {
//方法一:
return n > 0 && (n & (n - 1)) == 0;
//方法二:
return n > 0 && (n & (-n)) == n;
}
};
43.回文链表
给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
方法一:将值复制到数组中后用双指针法
class Solution {
public:
bool isPalindrome(ListNode* head) {
vector<int> v;
while(head != nullptr){
v.emplace_back(head->val);
head = head->next;
}
for(int i = 0, j = v.size() - 1; i < j; i++, j--){
if(v[i] != v[j]) return false;
}
return true;
}
};
方法二:递归
class Solution {
ListNode* pre;
public:
bool DG(ListNode* next) {
if(next != nullptr){
if(!DG(next->next)) return false; //让next逐渐指向链尾
if(pre->val != next->val) return false;
pre = pre->next;
}
return true;
}
bool isPalindrome(ListNode* head) {
pre = head;
return DG(head);
}
};
方法三:快慢指针
class Solution {
public:
//建立两个指针,一个快,一个慢
ListNode* sf(ListNode* head){
ListNode* slow = head;
ListNode* fast = head;
while(fast->next != nullptr && fast->next->next != nullptr){
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
//反转链表
ListNode* res(ListNode* head){
ListNode* pre = nullptr;
ListNode* cur = head;
while(cur){
ListNode* temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
bool isPalindrome(ListNode* head) {
if(head == nullptr) return true;
// 找到前半部分链表的尾节点并反转后半部分链表
ListNode* fist = sf(head); //fist指向前半部分末尾
ListNode* sec = res(fist->next); //sec指向反转后的开头
// 判断是否回文
ListNode* p1 = head;
ListNode* p2 = sec;
bool rest = true;
while(p2 && rest){
if(p1->val != p2->val) rest = false;
p1 = p1->next;
p2 = p2->next;
}
// 还原链表并返回结果
fist->next = res(sec);
return rest;
}
};
44.二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
方法一:两次遍历
class Solution {
public:
//存放路径
vector<TreeNode*> getpath(TreeNode* root, TreeNode* p){
vector<TreeNode*> path;
TreeNode* node = root;
while(node != p){
path.push_back(node);
if(p->val > node->val) node = node->right;
else node = node->left;
}
path.push_back(node);
return path;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
vector<TreeNode*> path_p = getpath(root, p);
vector<TreeNode*> path_q = getpath(root, q);
TreeNode* a;
for(int i = 0; i < path_p.size() && i < path_q.size(); i++){
if(path_p[i] == path_q[i]) a = path_p[i];
else break;
}
return a;
}
};
方法二:一次遍历
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
TreeNode* a = root;
while(true){
if(p->val > a->val && q->val > a->val) a = a->right;
else if(p->val < a->val && q->val < a->val) a = a->left;
else break;
}
return a;
}
};
45.有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
方法一:排序后比较
class Solution {
public:
bool isAnagram(string s, string t) {
if(s.size() != t.size()) return false;
sort(s.begin(), s.end());
sort(t.begin(), t.end());
return s == t;
}
};
方法二:哈希表
class Solution {
public:
bool isAnagram(string s, string t) {
if(s.size() != t.size()) return false;
vector<int> table(26, 0);
for(auto& s1 : s){
table[s1 - 'a']++;
}
for(auto& t1 : t){
table[t1 - 'a']--;
if(table[t1 - 'a'] < 0) return false;
}
return true;
}
46.二叉树的所有路径
给你一个二叉树的根节点 root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
方法一:深度优先搜索
class Solution {
public:
void getpath(TreeNode* root, string s, vector<string>& path){
if(root != nullptr){
s += to_string(root->val);
if(root->left == nullptr && root->right == nullptr) path.push_back(s);
else {
s += "->";
getpath(root->left, s, path);
getpath(root->right, s, path);
}
}
}
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> path;
getpath(root, "", path);
return path;
}
};
方法二:广度优先搜索
class Solution {
public:
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> path;
if(root == nullptr) return path;
queue<TreeNode*> t;
queue<string> s;
t.push(root);
s.push(to_string(root->val));
while(!t.empty()){
TreeNode* node = t.front();
string str = s.front();
t.pop();
s.pop();
if(node->left == nullptr && node->right == nullptr) {
path.push_back(str);
}
else{
if(node->left != nullptr){
t.push(node->left);
s.push(str + "->" + to_string(node->left->val));
}
if(node->right != nullptr){
t.push(node->right);
s.push(str + "->" + to_string(node->right->val));
}
}
}
return path;
}
};
47.各位相加
给定一个非负整数 num
,反复将各个位上的数字相加,直到结果为一位数。返回这个结果。
方法一:直接算
class Solution {
public:
int addDigits(int num) {
while(num >= 10){
int sum = 0;
while(num > 0){
sum += num % 10;
num = num / 10;
}
num = sum;
}
return num;
}
};
方法二: 数学思想
思路:
class Solution {
public:
int addDigits(int num) {
return (num - 1) % 9 + 1;
}
};
48.丑数
丑数 就是只包含质因数 2
、3
和 5
的正整数。给你一个整数 n
,请你判断 n
是否为 丑数 。如果是,返回 true
;否则,返回 false
。
方法一:数学
思路:一直对2, 3, 5取模,如果为1则是丑数,否则不是
class Solution {
public:
bool isUgly(int n) {
if(n <= 0) return false;
vector<int> s{2, 3, 5};
for(int n1 : s){
while(n % n1 == 0){
n = n / n1;
}
}
return n == 1;
}
};
49.丢失的数字
给定一个包含 [0, n]
中 n
个数的数组 nums
,找出 [0, n]
这个范围内没有出现在数组中的那个数。
方法一:排序后利用数组
将数组排序之后,即可根据数组中每个下标处的元素是否和下标相等,得到丢失的数字。
class Solution {
public:
int missingNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
int n = nums.size();
for(int i = 0; i < n; i++){
if(nums[i] != i) return i;
}
return n;
}
};
方法二:哈希集合
class Solution {
public:
int missingNumber(vector<int>& nums) {
unordered_set<int> s;
int n = nums.size();
for(int i = 0; i < n; i++){
s.insert(nums[i]);
}
int mess = -1;
for(int i = 0; i <= n; i++){
if(!s.count(i)) {
mess = i;
break;
}
}
return mess;
}
};
方法三:位运算
思路: 数组 nums 中有 n 个数,在这 n 个数的后面添加从 0 到 n 的每个整数,由于只有丢失的数才会只有一个,可通过按位异或运算得到丢失的数字。位异或运算 ⊕ 满足交换律和结合律,且对任意整数 x 都满足 x⊕x=0 和 x⊕0=x。
class Solution {
public:
int missingNumber(vector<int>& nums) {
int res = 0;
int n = nums.size();
for(int i = 0; i < n; i++){
res ^= nums[i];
}
for(int i = 0; i <= n; i++){
res ^= i;
}
return res;
}
};
方法四:数学
思路: 求前n项和,再减去数组和
class Solution {
public:
int missingNumber(vector<int>& nums) {
int n = nums.size();
int toal = n * (n + 1) / 2;
int sum = 0;
for(int i = 0; i < n; i++){
sum += nums[i];
}
return toal - sum;
}
};
50.第一个错误的版本
假设你有 n
个版本 [1, 2, ..., n]
,你想找出导致之后所有版本出错的第一个错误的版本。
方法一:二分查找
class Solution {
public:
int firstBadVersion(int n) {
int left = 1, right = n;
while (left < right) { // 循环直至区间左右端点相同
int mid = left + (right - left) / 2; // 防止计算时溢出
if (isBadVersion(mid)) { //为true,则可能是第一个错误版本
right = mid; // 答案在区间 [left, mid] 中
} else {
left = mid + 1; // 答案在区间 [mid+1, right] 中
}
}
// 此时有 left == right,区间缩为一个点,即为答案
return left;
}
};
注意:不能用(left+right)/2形式,当left和right都是int,两个值的初始值都超过int限定大小的一半,那么left+right就会发生溢出,所以应该用left+(right-left)/2来防止求中值时候的溢出。
51.移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
方法一:双指针
思路:使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。右指针不断移动,若不为0,则将值赋给左指针,最后再补0。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = 0, right = 0;
int n = nums.size();
while(right < n){
if(nums[right] != 0){
nums[left] = nums[right];
left++;
right++;
}
else right++;
}
while(left < n){
nums[left] = 0;
left++;
}
}
};
52.单词规律
方法一:哈希表
class Solution {
public:
bool wordPattern(string pattern, string s) {
//创建两个哈希表进行双射
unordered_map<string, char> s1;
unordered_map<char, string> t1;
int num = s.size();
int i = 0;
for(auto ch : pattern){
if(i > num) return false;
//空格作为边界,及空格前为一个单词
int j = i;
while(j < num && s[j] != ' ') j++;
//截取单词
const string &temp = s.substr(i, j-i);
//比较
if(s1.count(temp) && s1[temp] != ch) return false;
if(t1.count(ch) && t1[ch] != temp) return false;
//建立映射关系
s1[temp] =ch;
t1[ch] = temp;
i = j + 1;
}
return i >= num;
}
};
53.Nim 游戏
你和你的朋友,两个人一起玩 Nim 游戏:
- 桌子上有一堆石头。
- 你们轮流进行自己的回合, 你作为先手 。
- 每一回合,轮到的人拿掉 1 - 3 块石头。
- 拿掉最后一块石头的人就是获胜者。
假设你们每一步都是最优解。请编写一个函数,来判断你是否可以在给定石头数量为 n 的情况下赢得游戏。如果可以赢,返回 true;否则,返回 false 。
方法一:数学推理
class Solution {
public:
bool canWinNim(int n) {
//方法一:
return n % 4 != 0;
//方法二:
return (n & 3) != 0;
}
};
54.区域和检索 - 数组不可变
给定一个整数数组 nums,处理以下类型的多个查询:
- 计算索引 left 和 right (包含 left 和 right)之间的 nums 元素的 和 ,其中 left <= right
实现 NumArray 类:
- NumArray(int[] nums) 使用数组 nums 初始化对象
- int sumRange(int i, int j) 返回数组 nums 中索引 left 和 right 之间的元素的 总和 ,包含 left 和 right 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] 。
方法一:前缀和
class NumArray {
public:
vector<int> v;
NumArray(vector<int>& nums) {
int n =nums.size();
v.resize(n + 1);
for(int i = 0; i < n; i++){
v[i + 1] = v[i] + nums[i];
}
}
int sumRange(int i, int j) {
return v[j + 1] - v[i];
}
};
55.3 的幂
给定一个整数,写一个函数来判断它是否是 3 的幂次方。如果是,返回 true ;否则,返回 false 。整数 n 是 3 的幂次方需满足:存在整数 x 使得 n == 3x
方法一:试除法
class Solution {
public:
bool isPowerOfThree(int n) {
while(n && n % 3 == 0){
n /= 3;
}
return n == 1;
}
};
56.比特位计数
给你一个整数 n
,对于 0 <= i <= n
中的每个 i
,计算其二进制表示中 1
的个数 ,返回一个长度为 n + 1
的数组 ans
作为答案。
方法一:Brian Kernighan 算法
思路:最直观的做法是对从 0 到 n 的每个整数直接计算「一比特数」。每个 int 型的数都可以用 32 位二进制数表示,只要遍历其二进制表示的每一位即可得到 1 的数目。
class Solution {
public:
int get(int n){
int ones = 0;
while(n > 0){
n = n & (n - 1);
ones++;
}
return ones;
}
vector<int> countBits(int n) {
vector<int> v(n + 1);
for(int i = 0; i <= n ; i++){
v[i] = get(i);
}
return v;
}
};
方法二:动态规划——最高有效位
思路:以8为例,二进制表示是1 0 0 0,那么从8到15的过程中,最高位始终是1,只有后三位从0 0 0到1 1 1在变化
class Solution {
public:
vector<int> countBits(int n) {
vector<int> v(n + 1);
int height = 0;
for(int i = 1; i <= n; i++){
if((i & (i - 1)) == 0){
height = i;
}
v[i] = v[i - height] + 1;
}
return v;
}
};
方法三:动态规划——最低有效位
class Solution {
public:
vector<int> countBits(int n) {
vector<int> v(n + 1);
for(int i = 1; i <= n; i++){
v[i] = v[i >> 1] + (i & 1);
}
return v;
}
};
方法四:动态规划——最低设置位
class Solution {
public:
vector<int> countBits(int n) {
vector<int> v(n + 1);
for(int i = 1; i <= n; i++){
v[i] = v[i & (i - 1)] + 1;
}
return v;
}
};
57.4的幂
给定一个整数,写一个函数来判断它是否是 4 的幂次方。如果是,返回 true ;否则,返回 false 。整数 n 是 4 的幂次方需满足:存在整数 x 使得 n == 4x
class Solution {
public:
bool isPowerOfFour(int n) {
//方法一:
while(n && n % 4 == 0){
n /= 4;
}
return n == 1;
//方法二:
return n > 0 && (n & (n - 1)) == 0 && (n % 3) == 1;
//方法三:
return n > 0 && (n & (n - 1)) == 0 && (n & 0xaaaaaaaa) == 0;
}
};
58.反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
方法一:双指针
class Solution {
public:
void reverseString(vector<char>& s) {
int n = s.size();
for(int left = 0, right = n - 1; left < right; left++, right--){
swap(s[left], s[right]);
}
}
};
59.反转字符串中的元音字母
给你一个字符串 s
,仅反转字符串中的所有元音字母,并返回结果字符串。元音字母包括 'a'
、'e'
、'i'
、'o'
、'u'
,且可能以大小写两种形式出现。
方法一:双指针
class Solution {
public:
string reverseVowels(string s) {
auto cap = [c = "aeiouAEIOU"s](char ch){
return c.find(ch) != string::npos;
};
int n = s.size();
int i =0, j = n - 1;
while(i < j){
while(i < n && !cap(s[i])) i++;
while(j > 0 && !cap(s[j])) j--;
if(i < j){
swap(s[i], s[j]);
i++;
j--;
}
}
return s;
}
};
注解:
- string::npos是一个静态成员常量,表示size_t的最大值(Maximum value for size_t)。该值表示“直到字符串结尾”,作为返回值它通常被用作表明没有匹配。
-
C++新特性中的lambda表达式
-
对于operator " "s的用法
" "s是一种operator,在这里的目的是因为捕获里c++会自动推导变量类型,会把"aeiouAEIOU"推导成const char *const类型,但其实我们需要的是string类型,其实这里""s就是转成string类型等效于vowels = std::string("aeiouAEIOU")。
60.两个数组的交集
给定两个数组 nums1
和 nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
方法一:两个集合 --哈希表
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> s1, s2;
for(auto& num : nums1){
s1.insert(num);
}
for(auto& num : nums2){
s2.insert(num);
}
return get(s1, s2);
}
vector<int> get(unordered_set<int>& s1, unordered_set<int>& s2){
if(s1.size() > s2.size()){
get(s2, s1);
}
vector<int> v;
for(auto& num : s1){
if(s2.count(num)){
v.push_back(num);
}
}
return v;
}
};
方法二:排序 + 双指针
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
sort(nums1.begin(), nums1.end());
sort(nums2.begin(), nums2.end());
int l1 = nums1.size(), l2 = nums2.size();
int l = 0, r = 0;
vector<int> v;
while(l < l1 && r < l2){
int num1 = nums1[l], num2 = nums2[r];
if(num1 == num2){
if(!v.size() || num1 != v.back()){
v.push_back(num1);
}
l++;
r++;
}
else if(num1 < num2) l++;
else r++;
}
return v;
}
};
两个数组的交集 II
给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。
方法一:哈希表
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
if(nums1.size() > nums2.size()) return intersect(nums2, nums1);
unordered_map<int, int> m;
for(auto& num : nums1) ++m[num];
vector<int> v;
for(auto& num : nums2){
if(m.count(num)){
v.push_back(num);
--m[num];
if(m[num] == 0) m.erase(num);
}
}
return v;
}
};
方法二:排序 + 双指针
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
sort(nums1.begin(), nums1.end());
sort(nums2.begin(), nums2.end());
int l1 = nums1.size(), l2 =nums2.size();
int l = 0, r = 0;
vector<int> v;
while(l < l1 && r < l2){
int num1 = nums1[l], num2 = nums2[r];
if(num1 < num2) l++;
else if(num1 > num2) r++;
else{
v.push_back(num1);
l++;
r++;
}
}
return v;
}
};
61.有效的完全平方数
给定一个 正整数 num
,编写一个函数,如果 num
是一个完全平方数,则返回 true
,否则返回 false
。
方法一:使用内置的库函数
class Solution {
public:
bool isPerfectSquare(int num) {
int x = (int) sqrt(num);
return x * x == num;
}
};
方法二:暴力
class Solution {
public:
bool isPerfectSquare(int num) {
long x = 1, n = 1;
while(n <= num){
if(n == num) return true;
++x;
n = x * x;
}
return false;
}
};
方法三:二分查找
class Solution {
public:
bool isPerfectSquare(int num) {
int l = 0, r = num;
while(l <= r){
int mid = l + (r - l) / 2;
long n = (long) mid * mid;
if(n == num) return true;
else if(n > num) r = mid - 1;
else l = mid + 1;
}
return false;
}
};
62.赎金信
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。如果可以,返回 true ;否则返回 false 。magazine 中的每个字符只能在 ransomNote 中使用一次。
方法一:字符统计
class Solution {
public:
bool canConstruct(string r, string m) {
if(r.size() > m.size()) return false;
vector<int> v(26);
for(auto& ch : m) ++v[ch - 'a'];
for(auto& ch : r){
--v[ch - 'a'];
if(v[ch - 'a'] < 0) return false;
}
return true;
}
};
63.字符串中的第一个唯一字符
给定一个字符串 s
,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1
方法一:使用哈希表存储频数
class Solution {
public:
int firstUniqChar(string s) {
unordered_map<char, int> m;
for(char ch : s){
++m[ch];
}
for(int i = 0; i < s.size(); i++){
if(m[s[i]] == 1) return i;
}
return -1;
}
};
方法二:使用哈希表存储索引
class Solution {
public:
int firstUniqChar(string s) {
unordered_map<char, int> m;
for(int i = 0; i < s.size(); i++){
if(m.count(s[i])) m[s[i]] = -1;
else m[s[i]] = i;
}
int n = s.size();
for(auto [_, pos] : m){ // 遍历哈希表
if(pos != -1 && pos < n) n = pos;
}
if(n == s.size()) n = -1;
return n;
}
};
方法三:队列
class Solution {
public:
int firstUniqChar(string s) {
unordered_map<char, int> m;
queue<pair<char, int>> q;
int n = s.size();
for(int i = 0; i < n; i++){
if(!m.count(s[i])){
m[s[i]] = i;
q.emplace(s[i], i);
}
else {
m[s[i]] = -1;
while(!q.empty() && m[q.front().first] == -1){
q.pop();
}
}
}
return q.empty() ? -1 : q.front().second;
}
};
64.找不同
给定两个字符串 s
和 t
,它们只包含小写字母。字符串 t
由字符串 s
随机重排,然后在随机位置添加一个字母。请找出在 t
中被添加的字母。
方法一:排序后比较
class Solution {
public:
char findTheDifference(string s, string t) {
sort(s.begin(), s.end());
sort(t.begin(), t.end());
int n = s.size();
for(int i = 0; i < n; i++){
if(s[i] != t[i]) return t[i];
}
return t[n];
}
};
方法二:计数
class Solution {
public:
char findTheDifference(string s, string t) {
vector<int> v(26, 0);
for(auto& ch : s){
++v[ch - 'a'];
}
for(auto& ch : t){
--v[ch - 'a'];
if(v[ch - 'a'] < 0){
return ch;
}
}
return ' ';
}
};
方法三:求和
class Solution {
public:
char findTheDifference(string s, string t) {
int as = 0, at = 0;
for(auto& ch : s) as += ch;
for(auto& ch : t) at += ch;
return at - as;
}
};
方法四:位运算
class Solution {
public:
char findTheDifference(string s, string t) {
int res = 0;
for(auto& ch : s) res ^= ch;
for(auto& ch : t) res ^= ch;
return res;
}
};
65.判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
方法一:双指针
class Solution {
public:
bool isSubsequence(string s, string t) {
if(s.size() > t.size()) return false;
int l = 0, r = 0;
while(l < s.size() && r < t.size()) {
if(s[l] == t[r]) {
l++;
r++;
}
else r++;
}
return l == s.size();
}
};
方法二:动态规划
//dp解法
bool isSubsequence(string s, string t){
int n = s.length(),m = t.length();
//dp数组dp[i][j]表示字符串t以i位置开始第一次出现字符j的位置
vector<vector<int>> dp(m + 1,vector<int> (26,0));
//初始化边界条件,dp[i][j] = m表示t中不存在字符j
for(int i=0;i<26;i++){
dp[m][i] = m;
}
//从后往前递推初始化dp数组
for(int i = m - 1;i>=0;i--) {
for(int j=0;j<26;j++){
if(t[i] == 'a' + j){
dp[i][j] = i;
}else {
dp[i][j] = dp[i + 1][j];
}
}
}
int add = 0;
for(int i = 0;i<n;i++){
//t中没有s[i] 返回false
if(dp[add][s[i] - 'a'] == m){
return false;
}
//否则直接跳到t中s[i]第一次出现的位置之后一位
add = dp[add][s[i] - 'a'] + 1;
}
return true;
}
66.二进制手表
二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。
- 例如,下面的二进制手表读取 "3:25" 。
给你一个整数 turnedOn ,表示当前亮着的 LED 的数量,返回二进制手表可以表示的所有可能时间。你可以 按任意顺序 返回答案。
小时不会以零开头:
- 例如,"01:00" 是无效的时间,正确的写法应该是 "1:00" 。分钟必须由两位数组成,可能会以零开头:
- 例如,"10:2" 是无效的时间,正确的写法应该是 "10:02" 。
方法一:枚举时分
思路:我们可以枚举小时的所有可能值 [0,11],以及分钟的所有可能值 [0,59]
class Solution {
public:
vector<string> readBinaryWatch(int turnedOn) {
vector<string> v;
for(int h = 0; h < 12; h++){
for(int m = 0; m < 60; m++){
//__builtin_popcount()是GCC提供的内建函数,主要作用是计算x表示成二进制时有多少个1(32位)。
if(__builtin_popcount(h) + __builtin_popcount(m) == turnedOn){
v.push_back(to_string(h) + ":" + (m < 10 ? "0" : "") + to_string(m));
}
}
}
return v;
}
};
方法二:二进制枚举
思路:枚举所有 2^10=1024 种灯的开闭组合,即用一个二进制数表示灯的开闭,其高 4 位为小时,低 6 位为分钟。
class Solution {
public:
vector<string> readBinaryWatch(int turnedOn) {
vector<string> v;
for(int i = 0; i < 1024; i++){
int h= i >> 6, m = i & 63; // 用位运算取出高 4 位和低 6 位
if(h < 12 && m < 60 && __builtin_popcount(i) == turnedOn){
//__builtin_popcount()是GCC提供的内建函数,主要作用是计算x表示成二进制时有多少个1(32位)。
v.push_back(to_string(h) + ":" + (m < 10 ? "0" : "") + to_string(m));
}
}
return v;
}
};
67.左叶子之和
给定二叉树的根节点 root
,返回所有左叶子之和。
方法一:深度优先搜索
class Solution {
public:
//判断是否是叶子节点
bool isleafnode(TreeNode* root){
return !root->left && !root->right;
}
//深度优先
int dfs(TreeNode* root){
int sum = 0;
if(root->left){
sum += isleafnode(root->left) ? root->left->val : dfs(root->left);
}
if(root->right && !isleafnode(root->right)){
sum += dfs(root->right);
}
return sum;
}
int sumOfLeftLeaves(TreeNode* root) {
return root ? dfs(root) : 0;
}
};
方法二:广度优先搜索
class Solution {
public:
//判断是否是叶子节点
bool isleafnode(TreeNode* root){
return !root->left && !root->right;
}
//广度优先
int sumOfLeftLeaves(TreeNode* root) {
if(!root) return 0;
queue<TreeNode*> q;
q.push(root);
int ans = 0;
while(!q.empty()){
TreeNode* node = q.front();
q.pop();
//先左子树
if(node->left){
if(isleafnode(node->left)) ans += node->left->val;
else q.push(node->left);
}
//右子树
if(node->right) {
if(!isleafnode(node->right)) q.push(node->right);
}
}
return ans;
}
};
68.数字转换为十六进制数
给定一个整数,编写一个算法将这个数转换为十六进制数。对于负整数,我们通常使用 补码运算 方法。
注意:
- 十六进制中所有字母(a-f)都必须是小写。
- 十六进制字符串中不能包含多余的前导零。如果要转化的数为0,那么以单个字符'0'来表示;对于其他情况,十六进制字符串中的第一个字符将不会是0字符。
- 给定的数确保在32位有符号整数范围内。
- 不能使用任何由库提供的将数字直接转换或格式化为十六进制的方法。
方法一:位运算
class Solution {
public:
string toHex(int num) {
if(num == 0) return "0";
string s;
for(int i = 7; i >= 0; i--){
//0xf16进制
int val = (num >> (4 * i)) & 0xf;
if(s.size() > 0 || val > 0){
char c = val < 10 ? (char)('0' + val) : ('a' + val - 10);
s.push_back(c);
}
}
return s;
}
};
69.最长回文串
给定一个包含大写字母和小写字母的字符串 s ,返回 通过这些字母构造成的 最长的回文串 。在构造过程中,请注意 区分大小写 。比如 "Aa" 不能当做一个回文字符串。
方法一:贪心
思路:用v / 2 * 2取偶数,用ans作为标记,只加一次出现奇数次的字符
class Solution {
public:
int longestPalindrome(string s) {
unordered_map<char, int> map;
int n = 0;
for(auto& ch : s){
map[ch]++;
}
for(auto& ch : map){
int v = ch.second;
n += v / 2 * 2;
if(v % 2 == 1 && n % 2 == 0) n++;
}
return n;
}
};
方法二:一次循环
思路:先遍历所有所有字符,遍历过程中如果为偶数len直接+2,最后比较len与s.size(),如果len小直接+1
class Solution {
public:
int longestPalindrome(string s) {
vector<int> v(128, 0);
int len = 0;
for(auto& ch : s){
if(v[ch] & 1) len += 2;
v[ch]++;
}
if(len < s.size()) len++;
return len;
}
};
70.Fizz Buzz
给你一个整数 n ,找出从 1 到 n 各个整数的 Fizz Buzz 表示,并用字符串数组 answer(下标从 1 开始)返回结果,其中:
- answer[i] == "FizzBuzz" 如果 i 同时是 3 和 5 的倍数。
- answer[i] == "Fizz" 如果 i 是 3 的倍数。
- answer[i] == "Buzz" 如果 i 是 5 的倍数。
- answer[i] == i (以字符串形式)如果上述条件全不满足。
方法一:模拟 + 字符串拼接
class Solution {
public:
vector<string> fizzBuzz(int n) {
vector<string> answer;
for(int i = 1; i <= n; i++){
string curr;
if(i % 3 == 0) curr +="Fizz";
if(i % 5 == 0) curr +="Buzz";
if(curr.size() == 0) curr += to_string(i);
answer.emplace_back(curr);
}
return answer;
}
};
71.第三大的数
给你一个非空数组,返回此数组中 第三大的数 。如果不存在,则返回数组中最大的数。
方法一:排序
class Solution {
public:
int thirdMax(vector<int>& nums) {
sort(nums.begin(), nums.end(), greater<>());
for(int i = 1, j = 1; i < nums.size(); i++){
if(nums[i] != nums[i - 1] && ++j == 3) {
// 此时 nums[i] 就是第三大的数
return nums[i];
}
}
return nums[0];
}
};
方法二:有序集合
class Solution {
public:
int thirdMax(vector<int>& nums) {
set<int> s;
for(auto& ch : nums){
s.insert(ch);
if(s.size() > 3) s.erase(s.begin());
}
return s.size() == 3 ? *s.begin() : *s.rbegin();
}
};
注解:
- 在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序
- begin(); // 返回指向第一个元素的迭代器
- end(); // 返回指向迭代器的最末尾处(即最后一个元素的下一个位置)
-
rbegin()–返回指向集合中最后一个元素的反向迭代器
-
rend()–返回指向集合中第一个元素的反向迭代器
方法三:一次遍历
class Solution {
public:
int thirdMax(vector<int> &nums) {
long a = LONG_MIN, b = LONG_MIN, c = LONG_MIN;
for (long num : nums) {
if (num > a) {
c = b;
b = a;
a = num;
} else if (a > num && num > b) {
c = b;
b = num;
} else if (b > num && num > c) {
c = num;
}
}
return c == LONG_MIN ? a : c;
}
};
72.分割字符串的最大得分
给你一个由若干 0 和 1 组成的字符串 s ,请你计算并返回将该字符串分割成两个 非空 子字符串(即 左 子字符串和 右 子字符串)所能获得的最大得分。
「分割字符串的得分」为 左 子字符串中 0 的数量加上 右 子字符串中 1 的数量。
方法一:枚举每个分割点
class Solution {
public:
int maxScore(string s) {
int ans = 0;
for(int i = 1; i < s.size(); i++){
int score = 0;
for(int j = 0; j < i; j++){
if(s[j] == '0') score++;
}
for(int j = i; j < s.size(); j++){
if(s[j] == '1') score++;
}
ans = max(ans, score);
}
return ans;
}
};
方法二:两次遍历
思路:当移动分割线时,如果分割线右边为0时,左边加一,右边不变,整体加一;当分割线右边为1时, 左边不变,右边减一,整体减一。
class Solution {
public:
int maxScore(string s) {
int score = 0;
if(s[0] == '0') score++;
for(int i = 1; i < s.size(); i++){
if(s[i] == '1') score++;
}
int ans = score;
for(int i = 1; i < s.size() - 1; i++){
if(s[i] == '0') score++;
if(s[i] == '1') score--;
ans = max(ans, score);
}
return ans;
}
};
73.数组中重复的数字
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
方法一:哈希表
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
//第一种:
unordered_map<int, int> map;
for(int i = 0; i < nums.size(); i++){
if(map.find(nums[i]) != map.end()) return nums[i];
else map[nums[i]]++;
}
return -1;
//第二种:
unordered_set<int> s;
for(auto& ch : nums){
if(s.count(ch)) return ch;
s.insert(ch);
}
return -1;
}
};
方法二:排序
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size() - 1; i++){
if(nums[i] == nums[i + 1]) return nums[i];
}
return -1;
}
};
方法三::索引:
思路:因为出现的元素值 < nums.size(); 所以我们可以将见到的元素 放到索引的位置,如果交换时,发现索引处已存在该元素
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++){
while(nums[i] != i){
if(nums[nums[i]] == nums[i]) return nums[i];
int temp = nums[i];
nums[i] = nums[temp];
nums[temp] = temp;
}
}
return -1;
}
};
74.字符串相加
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。
方法一:计算
class Solution {
public:
string addStrings(string num1, string num2) {
string s;
int curr = 0, i = num1.size() - 1, j = num2.size() - 1;
while(i >= 0 || j >= 0 || curr != 0){
if(i >= 0) curr += num1[i--] - '0';
if(j >= 0) curr += num2[j--] - '0';
s += to_string(curr % 10);
curr /= 10;
}
reverse(s.begin(), s.end());
return s;
}
};
reverse() :反转字符串。
75.设计有序流
有 n 个 (id, value) 对,其中 id 是 1 到 n 之间的一个整数,value 是一个字符串。不存在 id 相同的两个 (id, value) 对。
设计一个流,以 任意 顺序获取 n 个 (id, value) 对,并在多次调用时 按 id 递增的顺序 返回一些值。
实现 OrderedStream 类:
- OrderedStream(int n) 构造一个能接收 n 个值的流,并将当前指针 ptr 设为 1 。
- String[] insert(int id, String value) 向流中存储新的 (id, value) 对。存储后:
- 如果流存储有 id = ptr 的 (id, value) 对,则找出从 id = ptr 开始的 最长 id 连续递增序列 ,并 按顺序 返回与这些 id 关联的值的列表。然后,将 ptr 更新为最后那个 id + 1 。
- 否则,返回一个空列表。
方法一:使用数组存储 + 遍历
class OrderedStream {
public:
OrderedStream(int n) {
stream.resize(n + 1);
prt = 1;
}
vector<string> insert(int idKey, string value) {
stream[idKey] = value;
vector<string> res;
while(prt < stream.size() && !stream[prt].empty()){
res.push_back(stream[prt]);
prt++;
}
return res;
}
private:
vector<string> stream;
int prt;
};
76.替换空格
请实现一个函数,把字符串 s
中的每个空格替换成"%20"。
方法一:字符数组
public:
string replaceSpace(string s) { //字符数组
//方法一:
string array; //存储结果
for(auto &c : s){ //遍历原字符串
if(c == ' '){
array.push_back('%');
array.push_back('2');
array.push_back('0');
}
else{
array.push_back(c);
}
}
return array;
方法二:
string replaceSpace(string s) {
string arry;
for(auto &c : s){
if(c == ' ') arry += "%20";
else arry += c;
}
return arry;
}
};
77.字符串中的单词数
统计字符串中的单词个数,这里的单词指的是连续的不是空格的字符。请注意,你可以假定字符串里不包括任何不可打印的字符。
方法一:末尾加空格直接统计
class Solution {
public:
int countSegments(string s) {
s += ' ';
int n = 0;
for(int i = 0; i < s.size(); i++){
if(s[i] != ' ' && s[i + 1] == ' ') n++;
}
return n;
}
};
78.从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
方法一:递归
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
if(!head) return{} ;
vector<int> a = reversePrint(head->next);
a.push_back(head->val);
return a;
}
};
方法二:反转链表
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> v;
if(head == nullptr) return v;
ListNode* curr = head->next;
ListNode* temp;
head->next = nullptr;
//反转链表
while(curr){
temp = curr->next;
curr->next = head;
head = curr;
curr = temp;
}
//遍历赋值
while(head){
v.push_back(head->val);
head = head->next;
}
return v;
}
};
方法三:栈
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
stack<int> s;
vector<int> v;
while(head){
s.push(head->val);
head = head->next;
}
while(!s.empty()){
v.push_back(s.top());
s.pop();
}
return v;
}
};
方法四:逆序数组
public:
vector<int> reversePrint(ListNode* head) {
vector<int> v;
while(head){
v.push_back(head->val);
head = head->next;
}
int len = v.size();
for(int i = 0; i < len / 2; i++){
swap(v[i], v[len - 1 - i]);
}
return v;
}
};
79.排列硬币
你总共有 n 枚硬币,并计划将它们按阶梯状排列。对于一个由 k 行组成的阶梯,其第 i 行必须正好有 i 枚硬币。阶梯的最后一行 可能 是不完整的。给你一个数字 n ,计算并返回可形成 完整阶梯行 的总行数。
方法一:二分查找
class Solution {
public:
int arrangeCoins(int n) {
int l = 1, r = n;
while(l < r){
int mid = l + (r - l + 1) / 2;
if((long long) mid * (mid + 1) <= (long long)2 * n) l = mid;
else r = mid - 1;
}
return l;
}
};
方法二:数学
class Solution {
public:
int arrangeCoins(int n) {
return (int) ((sqrt((long long) 8 * n + 1) - 1) / 2);
}
};
80.斐波那契数列
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
方法一:动态规划
class Solution {
public:
int fib(int n) {
int mod = 1000000007;
if(n < 2) return n;
int q = 0, p = 0, r = 1;
for(int i = 2; i <= n; i++){
q = p;
p = r;
r = (q + p) % mod;
}
return r;
}
};
方法二:矩阵快速幂
class Solution {
public:
const int MOD = 1000000007;
int fib(int n) {
if (n < 2) {
return n;
}
vector<vector<long>> q{{1, 1}, {1, 0}};
vector<vector<long>> res = pow(q, n - 1);
return res[0][0];
}
vector<vector<long>> pow(vector<vector<long>>& a, int n) {
vector<vector<long>> ret{{1, 0}, {0, 1}};
while (n > 0) {
if (n & 1) {
ret = multiply(ret, a);
}
n >>= 1;
a = multiply(a, a);
}
return ret;
}
vector<vector<long>> multiply(vector<vector<long>>& a, vector<vector<long>>& b) {
vector<vector<long>> c{{0, 0}, {0, 0}};
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
c[i][j] = (a[i][0] * b[0][j] + a[i][1] * b[1][j]) % MOD;
}
}
return c;
}
}
81.打印从1到最大的n位数
输入数字 n
,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
方法一:循环
class Solution {
public:
vector<int> printNumbers(int n) {
int end = pow(10, n);
vector<int> v;
for(int i = 1; i < end; i++){
v.push_back(i);
}
return v;
}
};
方法二:大数打印解法
class Solution {
public:
vector<int>res; //抛开leetcode,实际上res存储的应该是string型
string path; //存储一个n位数
void backTracking(int n){
//递归结束的条件:path为n位数
if (path.size() == n){
//去除0、00、000和高位的0(01、001...)
if (path[0] == '0'){
int ptr = 0;
while(ptr < path.size() && path[ptr] == '0'){
ptr++;
}
if (ptr != path.size()){
//为了通过leetcode,res存储的是int型,故要把path从string转成int
//实际res存储的应该是string型,不用把path转成int型
res.emplace_back(stoi(path.substr(ptr)));
}
}
else{
res.emplace_back(stoi(path));
}
return;
}
for(int i = 0; i < 10 ;++i){
//i + '0' 得到 'i',即把 i 从 int 转为 char 型
path.push_back(i + '0');
backTracking(n);
path.pop_back();
}
}
vector<int> printNumbers(int n) {
backTracking(n);
return res;
}
};
82.调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分。
方法一:双指针
class Solution {
public:
vector<int> exchange(vector<int>& nums) {
int i = 0, j = nums.size() - 1;
while(i < j){
while(i < j && (nums[i] & 1) == 1) i++;
while(i < j && (nums[j] & 1) == 0) j--;
swap(nums[i], nums[j]);
}
return nums;
}
};
83.二叉搜索树的第k大节点
给定一棵二叉搜索树,请找出其中第 k
大的节点的值。
方法一: 中序遍历倒序 为 递减序列
class Solution {
public:
int s = 0;
void get(TreeNode* root, int& k){
if(root == nullptr) return ;
get(root->right, k);
if(k < 0) return;
if(--k == 0) s = root->val;
get(root->left, k);
}
int kthLargest(TreeNode* root, int k) {
get(root, k);
return s;
}
};
83.链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
方法一:顺序查找
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode* temp = nullptr;
int n = 0;
//计算链表长度
for(temp = head; temp; temp = temp->next){
n++;
}
for(temp = head; n > k; n--){
temp = temp->next;
}
return temp;
}
};
方法二:双指针
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode* fast = head;
ListNode* slow = head;
while(fast && k > 0){
fast = fast->next;
k--;
}
while(fast){
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
84.顺时针打印矩阵
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
方法一:顺时针打印矩阵
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
//判断是否为空
if(matrix.empty()) return {};
vector<int> res;
int l = 0; //左边界
int r = matrix[0].size() - 1; //右边界
int t = 0; //上边界
int b = matrix.size() - 1; //下边界
//开始循环
while(true){
//left->right
for(int i = l; i <= r; i++){
res.push_back(matrix[t][i]);
}
//判断是否越界
if(++t > b) break;
//top->bottom
for(int i = t; i <= b; i++){
res.push_back(matrix[i][r]);
}
//判断是否越界
if(l > --r) break;
//right->left
for(int i = r; i >= l; i--){
res.push_back(matrix[b][i]);
}
//判断是否越界
if(t > --b) break;
//bottom->top
for(int i = b; i >= t; i--){
res.push_back(matrix[i][l]);
}
//判断是否越界
if(++l > r) break;
}
return res;
}
};
方法二:模拟
class Solution {
private:
static constexpr int directions[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.size() == 0 || matrix[0].size() == 0) {
return {};
}
int rows = matrix.size(), columns = matrix[0].size();
vector<vector<bool>> visited(rows, vector<bool>(columns));
int total = rows * columns;
vector<int> order(total);
int row = 0, column = 0;
int directionIndex = 0;
for (int i = 0; i < total; i++) {
order[i] = matrix[row][column];
visited[row][column] = true;
int nextRow = row + directions[directionIndex][0], nextColumn = column + directions[directionIndex][1];
if (nextRow < 0 || nextRow >= rows || nextColumn < 0 || nextColumn >= columns || visited[nextRow][nextColumn]) {
directionIndex = (directionIndex + 1) % 4;
}
row += directions[directionIndex][0];
column += directions[directionIndex][1];
}
return order;
}
};
方法三:按层模拟
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.size() == 0 || matrix[0].size() == 0) {
return {};
}
int rows = matrix.size(), columns = matrix[0].size();
vector<int> order;
int left = 0, right = columns - 1, top = 0, bottom = rows - 1;
while (left <= right && top <= bottom) {
for (int column = left; column <= right; column++) {
order.push_back(matrix[top][column]);
}
for (int row = top + 1; row <= bottom; row++) {
order.push_back(matrix[row][right]);
}
if (left < right && top < bottom) {
for (int column = right - 1; column > left; column--) {
order.push_back(matrix[bottom][column]);
}
for (int row = bottom; row > top; row--) {
order.push_back(matrix[row][left]);
}
}
left++;
right--;
top++;
bottom--;
}
return order;
}
};
85.包含min函数的栈
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
方法一:辅助栈
class MinStack {
private:
stack<int> s1; //存入栈元素
stack<int> s2; //存放递减的元素
public:
/** initialize your data structure here. */
MinStack() {
s2.push(INT_MAX);//初始化s2
}
void push(int x) {
s1.push(x);
if(x <= s2.top()){
s2.push(x);
}
}
void pop() {
if(s2.top() == s1.top()){
s2.pop();
}
s1.pop();
}
int top() {
return s1.top();
}
int min() {
return s2.top();
}
};
方法二:一个栈
class MinStack {
private:
int min1 = INT_MAX;
stack<int> s;
public:
/** initialize your data structure here. */
MinStack() {
}
void push(int x) {
s.push(min1); //放入上一个最小值
if(x < min1) min1 = x; //保证最小值
s.push(x); //放入该值
}
void pop() {
s.pop(); //出栈栈顶元素
min1 = s.top(); //得到出栈后的最小值
s.pop(); //出栈最小值
}
int top() {
return s.top();
}
int min() {
return min1;
}
};
方法三:动态维护
class MinStack {
private:
stack<pair<int, int>> s;
int min1 = INT_MAX;
public:
/** initialize your data structure here. */
MinStack() {}
void push(int x) {
min1 = ::min(min1, x);
s.push({x, min1});
}
void pop() {
s.pop();
if(s.empty())
min1 = INT_MAX;
else
min1 = s.top().second;
}
int top() {
return s.top().first;
}
int min() {
return s.top().second;
}
};
86.II. 从上到下打印二叉树 II
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
方法一:广度优先搜索
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
//定义
queue<TreeNode*> q;
vector<vector<int>> v;
if(root == nullptr) return v;
q.push(root);
while(!q.empty()){
vector<int> temp;
for(int i = q.size(); i > 0; i--){
TreeNode* node = q.front();
q.pop();
temp.push_back(node->val);
if(node->left != nullptr) q.push(node->left);
if(node->right != nullptr) q.push(node->right);
}
v.push_back(temp);
}
return v;
}
};
87.最小的k个数
输入整数数组 arr
,找出其中最小的 k
个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
方法一: 排序
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
sort(arr.begin(), arr.end());
vector<int> v;
for(int i = 0; i < k; i++){
v.push_back(arr[i]);
}
return v;
}
};
方法二:堆
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
vector<int> v;
priority_queue<int> q;
//priority_queue(),默认按照从小到大排列。所以top()返回的是最大值而不是最小值!
//使用greater<>后,数据从大到小排列,top()返回的就是最小值而不是最大值!
//如果使用了第三个参数,那第二个参数不能省,用作保存数据的容器!!!!
if(k == 0) return v;
//先存k个值到优先队列中
for(int i = 0; i < k; i++){
q.push(arr[i]);
}
//筛选最小的k个值
for(int i = k; i < (int)arr.size(); i++){
if(arr[i] < q.top()){
q.pop();
q.push(arr[i]);
}
}
//存到v中
for(int i = 0; i < k; i++){
v.push_back(q.top());
q.pop();
}
return v;
}
};
方法三:快速排序
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
vector<int> v;
//调用快排
quicksort(arr, 0, arr.size() - 1);
//assign拷贝
v.assign(arr.begin(), arr.begin() + k);
return v;
}
private:
//快速排序
void quicksort(vector<int>& arr, int l, int r){
//子数组长度为1时,递归结束
if(l >= r) return;
// 哨兵划分操作(以 arr[l] 作为基准数)
int i = l, j = r;
//将比基准数大的值放到右边,小的值放到左边
while(i < j){
while(i < j && arr[j] >= arr[l]) j--;
while(i < j && arr[i] <= arr[l]) i++;
swap(arr[i], arr[j]);
}
//交换基准数
swap(arr[i], arr[l]);
// 递归左(右)子数组执行哨兵划分
quicksort(arr, l, i - 1);
quicksort(arr, i + 1, r);
}
};
方法四: 基于快速排序的数组划分
思路:
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
if(k >= arr.size()) return arr;
return quicksort(arr, k, 0, arr.size() - 1);
}
private:
//搜索并返回最小的 k 个数
vector<int> quicksort(vector<int>& arr, int k, int l, int r){
// 哨兵划分操作(以 arr[l] 作为基准数)
int i = l, j = r;
//将比基准数大的值放到右边,小的值放到左边
while(i < j){
while(i < j && arr[j] >= arr[l]) j--;
while(i < j && arr[i] <= arr[l]) i++;
swap(arr[i], arr[j]);
}
//交换基准数
swap(arr[i], arr[l]);
//若 k < i ,代表第 k + 1小的数字在左子数组中,则递归左子数组;
//若 k > i ,代表第 k + 1 小的数字在右子数组中,则递归右子数组;
//若 k = i ,代表此时 arr[k] 即为第 k + 1 小的数字,则直接返回数组前 k 个数字即可;
if(i > k) return quicksort(arr, k, l, i - 1);
if(i < k) return quicksort(arr, k, i + 1, r);
vector<int> v;
v.assign(arr.begin(), arr.begin() + k);
return v;
}
};
88.第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
方法一:哈希表
class Solution {
public:
char firstUniqChar(string s) {
unordered_map<char, bool> map;
for(auto ch : s){
map[ch] = map.find(ch) == map.end();
}
for(auto ch : s){
if(map[ch]) return ch;
}
return ' ';
}
};
方法二:有序哈希表
public:
char firstUniqChar(string s) {
unordered_map<char, bool> map;
vector<char> v;
for(auto ch: s){
if(map.find(ch) == map.end()) v.push_back(ch);
map[ch] = map.find(ch) == map.end();
}
for(auto ch : v){
if(map[ch]) return ch;
}
return ' ';
}
};
89.在排序数组中查找数字 I
统计一个数字在排序数组中出现的次数。
方法一:二分查找
class Solution {
public:
int search(vector<int>& nums, int target) {
return get(nums, target) - get(nums, target - 1);
}
int get(vector<int> n, int tar){
int i = 0, j = n.size() - 1;
while(i <= j){
int mid = i + (j - i) / 2;
if(n[mid] <= tar) i = mid + 1;
else j = mid - 1;
}
return i;
}
};
90.二叉树的深度
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
方法一:深度优先搜索(DFS)
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root == nullptr) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
};
方法二:广度优先搜索(BFS)
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root == nullptr) return 0;
queue<TreeNode*> q;
int count = 0;
q.push(root);
while(!q.empty()){
int s = q.size();//每层的节点数
while(s > 0){
TreeNode* temp = q.front();
q.pop();
if(temp->left != nullptr) q.push(temp->left);
if(temp->right != nullptr) q.push(temp->right);
s --; //当s=0时,表示该层遍历结束
}
count++;
}
return count;
}
};
91.翻转单词顺序
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",则输出"student. a am I"。
方法一:双指针+分割
class Solution {
public:
string reverseWords(string s) {
string res;
int n = s.size();
int r = n - 1;
if(n == 0) return res;
while(r >= 0){
//从后往前寻找字符
while(r >= 0 && s[r] == ' ') r--;
if(r < 0) break;
//从后往前寻找空格
int l = r;
while(l >= 0 && s[l] != ' ') l--;
//添加单词
res += s.substr(l + 1, r - l);
res += ' ';
//继续分割单词
r = l;
}
//去除最后一个字符空格
if(!res.empty()) res.pop_back();
return res;
}
};