1.两数之和
给定一个整数数组nums和一个整数目标值target,请你在该数组中找出 和为目标值target的那两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
主要思想:利用哈希表的键值对特性,将元素-索引组合成键值对,遍历一遍数组,对于每个元素在哈希表中寻找是否存在一个键和当前元素的和为target,如果存在直接返回这两个元素的索引,如果不存在则将该值加入到哈希表中
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// 定义一个无序的哈希表(不会自动顺序排列),用于查找
// 哈希表的键为输入的元素,值为元素的索引
unordered_map<int,int> tmp_map;
int n = nums.size();
for(int i=0;i<n;i++)
{
// 定义一个迭代器,在哈希表中寻找和当前元素和为target的元素,如果找不到迭代器的地址会指向end
auto it = tmp_map.find(target - nums[i]);
// 判断迭代器的地址是否指向了end,如果不是说明在哈希表中找到了对应元素,返回结果
if(it != tmp_map.end())
return {i,it->second};
// 如果没有找到,it指向了end,则添加一组键值对
tmp_map[nums[i]] = i;
}
return {};
}
};
2. 有效的括号
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
核心思想:利用了栈的特性,将左括号存入栈中,当遇到相匹配的左括号则弹出,匹配关系通过哈希表查找,最后判断如果栈为空说明s中的左右括号都一一匹配了。
class Solution {
public:
bool isValid(string s) {
// 因为括号是一一对应的,所以先判断字符串中的字符是否为偶数,不是则直接返回错误
int n = s.size();
if (n % 2 == 1) {
return false;
}
// 定义一个哈希表,元素就是一组括号,用于查找,键为右括号,值为左括号
unordered_map<char, char> pairs = {
{')', '('},
{']', '['},
{'}', '{'}
};
// 定义一个栈,目的是当找到一组和左括号对应的右括号时就将其弹出,
// 最终如果栈为空说明每一个左括号都找到了对应的右括号
stack<char> stk;
// 遍历string字符串s中的每一个字母
for (char ch: s)
{
// count ()函数本质上检查unordered_map中是否存在具有给定键的元素
// 如果存在给定键的值,则返回1,否则返回0,
// 如果给的是左括号,哈希表中没有这个键,则将该左括号放入栈
// 如果给的是右括号,则就要在栈中找有没有对应的左括号
// 如果栈为空,则右括号不合法,如果栈的top不是右括号,则括号的顺序乱了
if (pairs.count(ch))
{
if (stk.empty() || stk.top() != pairs[ch]) {
return false;
}
stk.pop();
}
else
{
stk.push(ch);
}
}
return stk.empty();
}
};
3.合并两个有序链表
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
主要运行递归的思想:递归的访问两个链表中的元素,然后比较大小,然后更新指针。因为头结点已经判断结束了,因此需要更新的是当前节点的next指向的节点。
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
// 如果l1为空返回l2,反之返回l1
if (l1 == NULL) {
return l2;
}
if (l2 == NULL) {
return l1;
}
// 判断l1和l2哪个的头结点比较小
if (l1->val <= l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
};
4.爬楼梯
假设你正在爬楼梯。需要n阶你才能到达楼顶。
每次你可以爬1或2个台阶。你有多少种不同的方法可以爬到楼顶呢?
1 <= n <= 45
动态规划思想:它意味着爬到第x级台阶的方案数是爬到第x−1级台阶的方案数和爬到第x−2级台阶的方案数的和。
利用滚动数组求解,f(x) = f(x-1) + f(x-2),也就是进行一次遍历,然后每次更新f(x-1)和f(x-2)即可。
class Solution {
public:
int climbStairs(int n) {
if(n<=2)
return n;
int first=1,second=2;
for(int i=2;i<n;++i)
{
int third = first + second;
first = second;
second = third;
}
return second;
}
};
5.二叉树的中序遍历
给定一个二叉树的根节点 root ,返回它的中序遍历。
二叉树的中序遍历:按照访问左子树——根节点——右子树的方式遍历这棵树,而在访问左子树或者右子树的时候我们按照同样的方式遍历,直到遍历完整棵树。因此整个遍历过程天然具有递归的性质,我们可以直接用递归函数来模拟这一过程。
class Solution {
public:
void inorder(TreeNode* root, vector<int>& res) {
// 如果二叉树为空直接返回空值
if (!root) {
return;
}
// 先访问左子树(这时候又要递归,访问左子树的根节点直到为空)
inorder(root->left, res);
// 再访问根节点,根节点就1个,不需要递归
res.push_back(root->val);
// 最后访问右子树,对右子树的每个节点也是左节点-根节点-右节点的顺序
inorder(root->right, res);
}
// 递归main 函数
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
// 递归调用根节点
inorder(root, res);
return res;
}
};
6.检查是否为对称二叉树
给你一个二叉树的根节点root,检查它是否轴对称。
如果一个树的左子树与右子树镜像对称,那么这个树是对称的。
因此,该问题可以转化为:两个树在什么情况下互为镜像?
如果同时满足下面的条件,两个树互为镜像:
- 它们的两个根结点具有相同的值
- 每个树的右子树都与另一个树的左子树镜像对称
我们可以实现这样一个递归函数,通过「同步移动」两个指针的方法来遍历这棵树,p指针和q指针一开始都指向这棵树的根,随后p右移时,q左移,p左移时,q右移。每次检查当前p和q节点的值是否相等,如果相等再判断左右子树是否对称。
class Solution {
public:
// 递归函数,如果左子树和右子树均为空则对称,如果有一个为空一个不为空则不对称
bool check(TreeNode *p, TreeNode *q) {
// 左子树和右子树均为空的情况
if (!p && !q) return true;
// 左子树和右子树只有一个为空
if ((!p && q) || (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);
}
};
7.求二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
使用深度优先搜索的方法
如果我们知道了左子树和右子树的最大深度r,l,那么该二叉树的最大深度即max(r,l)+1
而左子树和右子树的最大深度又可以以同样的方式进行计算。因此我们可以用「深度优先搜索」的方法来计算二叉树的最大深度。具体而言,在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在O(1) 时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
};
8.买股票的最佳时机
给定一个数组prices,它的第i个元素prices[i]表示一支给定股票第i天的价格。
你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0 。
我们需要找出给定数组中两个数字之间的最大差值(即,最大利润)。此外,第二个数字(卖出价格)必须大于第一个数字(买入价格)。
主要思想:遍历整个数组,一遍寻找最小值,一遍计算每一个值和上一个最小值之间的最大差值。
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;
}
};
9.只出现一次的数字
给你一个非空整数数组nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
答案是使用位运算。对于这道题,可使用异或运算
异或运算的性质:
- 任何数和0做异或运算,结果仍然是原来的数
- 任何数和其自身做异或运算,结果是0
- 异或运算满足交换律和结合律
因为虽然for循环中的异或运算是按顺序逐次进行的,但是等同于将所有的元素放一起做异或运算,然后结合交换律,所有相同元素的异或均为0,然后和出现一次的元素异或为该元素
class Solution {
public:
int singleNumber(vector<int>& nums) {
int x=0;
for(int n:nums)
{
x = x^n;
}
return x;
}
};
10.给定一个链表的头结点判断是否有环
最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。
具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。
class Solution {
public:
bool hasCycle(ListNode *head) {
// 定义无序的set
unordered_set<ListNode*> seen;
while (head != nullptr) {
// 判断是否存在head对应的值
if (seen.count(head)) {
return true;
}
// 不存在则插入
seen.insert(head);
// 访问下一个值
head = head->next;
}
return false;
}
};
11.找出两个链表相交的起始点
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。判断两个链表是否相交,可以使用哈希集合存储链表节点。
首先遍历链表headA,并将链表headA中的每个节点加入哈希集合中。然后遍历链表headB,对于遍历到的每个节点,判断该节点是否在哈希集合中:
如果当前节点不在哈希集合中,则继续遍历下一个节点;
如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。
如果链表headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回null。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
unordered_set<ListNode *> visited;
ListNode *temp = headA;
while (temp != nullptr) {
visited.insert(temp);
temp = temp->next;
}
temp = headB;
while (temp != nullptr) {
if (visited.count(temp)) {
return temp;
}
temp = temp->next;
}
return nullptr;
}
};
12.返回多数元素
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
核心思想:将元素和元素出现的次数作为键值对放入map
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int, int> counts;
int majority = 0, cnt = 0;
for (int num: nums) {
++counts[num];// 一次++操作代表将元素插入了map容器
if (counts[num] > cnt) {
majority = num;
cnt = counts[num];
}
}
return majority;
}
};
13.反转单链表
给你单链表的头节点head,请你反转链表,并返回反转后的链表。
首先将链表中的第一个元素拿出来指向空集,然后将第二个元素拿出来指向第一个元素,然后。。。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
};
14.反转二叉树
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
这是一道很经典的二叉树问题。显然,我们从根节点开始,递归地对树进行遍历,并从叶子节点先开始翻转。如果当前遍历到的节点
root 的左右两棵子树都已经翻转,那么我们只需要交换两棵子树的位置,即可完成以root为根节点的整棵子树的翻转。
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(nullptr==root)
return nullptr;
TreeNode* left = invertTree(root->left);
TreeNode* right = invertTree(root->right);
root->left = right;
root->right = left;
return root;
}
};
15.判断是否为回文链表
给你一个单链表的头节点head,请你判断该链表是否为回文链表。如果是,返回true ;否则,返回false 。
一共为两个步骤:
复制链表值到数组列表中。
使用双指针法判断是否为回文。
class Solution {
public:
bool isPalindrome(ListNode* head) {
vector<int> new_vec;
while(head)
{
new_vec.push_back(head->val);
head = head->next;
}
if(!new_vec.size()/2)
return false;
int nsize = new_vec.size();
for(size_t i=0;i<new_vec.size()/2;i++)
{
int n1 = new_vec[i];
int n2 = new_vec[nsize-i-1];
if(n1==n2)
continue;
else
return false;
}
return true;
}
};
16.将数组中的0全部移动到数组尾部
使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。
右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。
注意到以下性质:
左指针左边均为非零数;
右指针左边直到左指针处均为零。
因此每次交换,都是将左指针的零与右指针的非零数交换,且非零数的相对顺序并未改变;
每当右指针指向非零时,左右指针同时移动;当右指针指向0时,只移动右指针
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left=0,right=0;
while(right<nums.size())
{
if(nums[right])
{
swap(nums[left],nums[right]);
left++;
}
right++;
}
}
};
17.比特位计数
给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。
Brian Kernighan 算法
对于任意整数x,令x=x&(x-1),该运算符将x的二进制表示的最后一个1变成0,因此对x重复该操作,直到x变成0,则操作次数即为x的一比特数
class Solution {
public:
int countones(int s)
{
int ones=0;
while(s>0)
{
s = s&(s-1);
ones++;
}
return ones;
}
vector<int> countBits(int n) {
vector<int> vbits(n+1);
for(int i=0;i<=n;i++)
{
vbits[i] = countones(i);
}
return vbits;
}
};
18.找到所有数组中消失的数字
给你一个含n个整数的数组nums,其中nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果
核心思想是将nums中的元素对应索引的元素加上n,最后统计小于等于n的元素的坐标
class Solution {
public:
vector<int> findDisappearedNumbers(vector<int>& nums) {
vector<int> res;
int n = nums.size();
for(int num:nums)
{
int x = (num-1)%n;
nums[x]+=n;
}
for(int i=0;i<nums.size();i++)
{
if(nums[i]<=n)
res.push_back(i+1);
}
return res;
}
};
19.两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
给你两个整数 x 和 y,计算并返回它们之间的汉明距离。
思路:对x和y使用异或运算,然后按位操作统计1的个数
class Solution {
public:
int hammingDistance(int x, int y) {
int s = x^y;
int ret=0;
while(s>0)
{
ret++;
s = s&(s-1);
}
return ret;
}
};
20.计算二叉树的直径
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
首先我们知道一条路径的长度为该路径经过的节点数减一,所以求直径(即求路径长度的最大值)等效于求路径经过节点数的最大值减一。
而任意一条路径均可以被看作由某个节点为起点,从其左儿子和右儿子向下遍历的路径拼接得到。
class Solution {
public:
int ans;
int computedepth(TreeNode* tr)
{
if(NULL == tr){
return 0;
}
int left = computedepth(tr->left);
int right = computedepth(tr->right);
ans = max(ans,left+right+1);
return max(left,right)+1;
}
int diameterOfBinaryTree(TreeNode* root) {
ans=1;
computedepth(root);
return ans-1;
}
};
21.合并两个二叉树
可以使用深度优先搜索合并两个二叉树。从根节点开始同时遍历两个二叉树,并将对应的节点进行合并。
两个二叉树的对应节点可能存在以下三种情况,对于每种情况使用不同的合并方式。
如果两个二叉树的对应节点都为空,则合并后的二叉树的对应节点也为空;
如果两个二叉树的对应节点只有一个为空,则合并后的二叉树的对应节点为其中的非空节点;
如果两个二叉树的对应节点都不为空,则合并后的二叉树的对应节点的值为两个二叉树的对应节点的值之和,此时需要显性合并两个节点。
对一个节点进行合并之后,还要对该节点的左右子树分别进行合并。这是一个递归的过程
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
if(nullptr==root1)
return root2;
if(nullptr==root2)
return root1;
TreeNode* merge_root = new TreeNode(root1->val+root2->val);
merge_root->left = mergeTrees(root1->left,root2->left);
merge_root->right = mergeTrees(root1->right,root2->right);
return merge_root;
}
};