一、两数之和
难度:简单
题目:
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
https://leetcode-cn.com/problems/two-sum/
我的题解:
点击查看代码
//时间复杂度为O(N^2)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> a;
int i,j;
int n=nums.size();
for(i=0;i<n;i++){
for(j=1;j<n;j++){//优化:for(j=i+1;j<n;j++){ //排除了i==j的情况,减少遍历次数
if(nums[i]+nums[j]==target&&i!=j){
a.push_back(i);
a.push_back(j);
return a;
}
}
}
return a;
}
};
点击查看代码
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hashtable;
for (int i = 0; i < nums.size(); ++i) {
auto it = hashtable.find(target - nums[i]);//find()返回一个指向target - nums[i]的迭代器
if (it != hashtable.end()) {
return {it->second, i};//返回特定键所对的值
}
hashtable[nums[i]] = i;//使用[ ]进行单个插入,括号内为特定键,若已存在键值,则赋值修改,若无则插入。
}
return {};
}
};
2.使用哈希表,可以将查找的时间复杂度降低到从O(N)降低到O(1)。
3.C++中有关Map的操作。
map: #include < map >
unordered_map: #include < unordered_map >
map: map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。
unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。
C++ Map常见用法说明:
点击查看代码
#include <iostream>
#include <unordered_map>
#include <map>
#include <string>
using namespace std;
int main()
{
//注意:C++11才开始支持括号初始化
unordered_map<int, string> myMap={{ 5, "张大" },{ 6, "李五" }};//使用{}赋值
myMap[2] = "李四"; //使用[ ]进行单个插入,若已存在键值2,则赋值修改,若无则插入。
myMap.insert(pair<int, string>(3, "陈二"));//使用insert和pair插入
//遍历输出+迭代器的使用
auto iter = myMap.begin();//auto自动识别为迭代器类型unordered_map<int,string>::iterator
while (iter!= myMap.end())
{
cout << iter->first << "," << iter->second << endl;
++iter;
}
//查找元素并输出+迭代器的使用
auto iterator = myMap.find(2);//find()返回一个指向2的迭代器
if (iterator != myMap.end())
cout << endl<< iterator->first << "," << iterator->second << endl;
system("pause");
return 0;
}
二、合并两个有序链表
难度:简单
题目:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
https://leetcode-cn.com/problems/merge-two-sorted-lists/
递归解法:
之所以适合用到递归是因为题目的思想是找到最小的节点,连接次小的节点;这个不断寻找最小值的过程适合用递归去实现。
点击查看代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(l1==nullptr){
return l2;
}
else if(l2==nullptr){
return l1;
}
else if(l1->val<l2->val){
l1->next=mergeTwoLists(l1->next,l2);
return l1;
}
else{
l2->next=mergeTwoLists(l2->next,l1);
return l2;
}
}
};
迭代解法:
思路
使用 dummy->next 来保存需要返回的头节点。
判断 l1 和 l2 哪个更小,就把这个节点接到下一个。
使用指向指针的指针 pp 用来存储更小的一边的指针。
在帮助 dummy 连接之后,还可以控制更小的 l1 或 l2 向后移动。
直到有一边为 nullptr ,即可将另一边剩余的都接上。
点击查看代码
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
while (l1 != nullptr && l2 != nullptr) {
ListNode** pp = (l1->val < l2->val) ? &l1 : &l2;
cur->next = *pp;
cur = cur->next;
*pp = (*pp)->next;
}
cur->next = (l1 == nullptr) ? l2 : l1;
ListNode* ans = dummy->next;
delete dummy;
return ans;
}
三、整数反转
难度:简单
题目:你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1] ,就返回 0。
https://leetcode-cn.com/problems/reverse-integer/
我的思路:将整数转换为字符串,再将字符串反转。
更好的解题思路:
每次取末尾数字:x%10
取完后将x去掉末尾数字:x/10
点击查看代码
class Solution {
public:
int reverse(int x) {
int res = 0;
while(x!=0) {
//每次取末尾数字
int tmp = x%10;
//判断是否 大于 最大32位整数
if (res>214748364 || (res==214748364 && tmp>7)) {
return 0;
}
//判断是否 小于 最小32位整数
if (res<-214748364 || (res==-214748364 && tmp<-8)) {
return 0;
}
res = res*10 + tmp;
x /= 10;
}
return res;
}
};
四、有效的括号
难度:简单
题目:给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1] ,就返回 0。
用到主要的方法:栈,map
思路:
1.若字符数串为单数可直接判断是错误的。
2.括号匹配的查找:运用map形成键值对;
3.将字符串按单个字符进行遍历 for (char ch: s)
4.建立栈: stack<栈元素类型> stk; 若为左括号则压栈,若为右括号,栈空则判断错;栈不空,取栈顶元素看是否与当前字符对应的另一半括号匹配;若匹配,栈顶元素出栈;
5.若栈为空,表达式正确。
点击查看代码
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;
for (char ch: s) {
if (pairs.count(ch)) {
if (stk.empty() || stk.top() != pairs[ch]) {// pairs[ ]:用于通过键查找对应值
return false;
}
stk.pop();
}
else {
stk.push(ch);
}
}
return stk.empty();
}
};
四、爬楼梯
难度:简单
题目:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?注意:给定 n 是一个正整数。
方法:动态规划
我的题解:
点击查看代码
class Solution {
public:
int climbStairs(int n) {
int f[n+1];
f[0]=1;
f[1]=2;
for(int i =2;i<n;i++){
f[i]=f[i-1]+f[i-2];
}
return f[n-1];
}
};
五、最大子序和
难度:简单
题目:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
方法:动态规划
难点在于转移方程的构建:f(i)表示以第i个数结尾的最大子序和;f(i)=max{f(i-1)+nums[i],nums[i]};
我的题解:
点击查看代码
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n=nums.size();
int f[n];
f[0]=nums[0];
for(int i=1;i<n;i++){
f[i]=max(f[i-1]+nums[i],nums[i]);
}
int answer=-2147483648;
for(int i=0;i<n;i++){
answer=max(f[i],answer);
}
return answer;
}
};
六、二叉树的中序遍历
难度:简单
题目:给定一个二叉树的根节点 root ,返回它的中序遍历。
方法:递归
点击查看代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void inorder(TreeNode* root,vector<int> &answer){
if(!root){return; }
inorder(root->left, answer);
answer.push_back(root->val);
inorder(root->right, answer);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> answer;
inorder(root,answer);
return answer;
}
};
总结:1.这是一个简单基础的算法,必须掌握背诵。
2.C++中vector<int>& nums和vector<int> nums的区别:
当vector当作形参输入到函数时,有两种方法:
vector
vector
带&表示传入函数的是vector的引用(即物理位置),函数内部对vector改动,vector就会改变;
不带&表示传入的是vector的复制品(开辟了另一块位置),函数内部对其改动,不会影响原本的vector;
七、盛水最多的容器
难度:中等
标签:Array
题目:Given n non-negative integers a1, a2, ..., an , where each represents a point at coordinate (i, ai). n vertical lines are drawn such that the two endpoints of the line i is at (i, ai) and (i, 0). Find two lines, which, together with the x-axis forms a container, such that the container contains the most water.
Notice that you may not slant the container.
Example 1:
Input: height = [1,8,6,2,5,4,8,3,7]
Output: 49
Explanation: The above vertical lines are represented by array [1,8,6,2,5,4,8,3,7]. In this case, the max area of water (blue section) the container can contain is 49.
https://leetcode.com/problems/container-with-most-water/
方法:双指针
我的题解:两层循环(超时了)
点击查看代码
class Solution {
public:
int maxArea(vector<int>& height) {
int max_area=0;
int i,j;
int n=height.size();
int longth=0;
for(i=0;i<n;i++){
for(j=i+1;j<n;j++){
longth=min(height[i],height[j]);
if((j-i)*longth>max_area){
max_area=(j-i)*longth;
}
}
}
return max_area;
}
}
官方题解:
点击查看代码
class Solution {
public:
int maxArea(vector<int>& height) {
int l = 0, r = height.size() - 1;
int ans = 0;
while (l < r) {//当两指针重合循环结束
int area = min(height[l], height[r]) * (r - l);//height[l], height[r]:最开始两个指针分别指向数组的两头
ans = max(ans, area);//取每次以双指针为左右边界(也就是「数组」的左右边界)计算出的容量中的最大值
if (height[l] <= height[r]) {//若左指针的值比右指针小,将左指针向右移动;若右指针的值比左指针大,将右指针向左移动
++l;
}
else {
--r;
}
}
return ans;
}
};
时间复杂度:O(N)O(N),双指针总计最多遍历整个数组一次。
空间复杂度:O(1)O(1),只需要额外的常数级别的空间。
总结:这道题最优的做法是使用「双指针」,但是怎么能看出用双指针最优呢???
双指针代表的是 可以作为容器边界的所有位置的范围。在一开始,双指针指向数组的左右边界,表示 数组中所有的位置都可以作为容器的边界,因为我们还没有进行过任何尝试。在这之后,我们每次将 对应的数字较小的那个指针 往 另一个指针 的方向移动一个位置,就表示我们认为 这个指针不可能再作为容器的边界了。
七、三数之和
难度:中等
标签:Array
题目:Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0.Notice that the solution set must not contain duplicate triplets.
https://leetcode.com/problems/3sum/
方法:双指针
当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,将枚举的时间复杂度从O(N^2)减少至O(N)。
解题思路:
1.除去重复:因为不能重复,所以将数组排序,使得枚举出的组合为(a,b,c)(a>=b>=c),保证了只有 (a, b, c)(a,b,c) 这个顺序会被枚举到,而 (b, a, c)(b,a,c)、(c, b, a)(c,b,a) 等等这些不会,这样就减少了重复。
2.跳出三重循环的大框架:
如果我们固定了前两重循环枚举到的元素 a 和 b,那么只有唯一的 c 满足 a+b+c=0。当第二重循环往后枚举一个元素b'时,由于 b' > b ,那么满足 a+b'+c'=0,a+b′+c'=0 的 c' 一定有 c' < c,即 c' 在数组中一定出现在c的左侧。也就是说,我们可以从小到大枚举b,同时从大到小枚举c,即第二重循环和第三重循环实际上是并列的关系。(双指针)有了这样的发现,我们就可以保持第二重循环不变,而将第三重循环变成一个从数组最右端开始向左移动的指针。
点击查看代码
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> answer;
int n=nums.size();
int sum=0;
int l=0;
sort(nums.begin(), nums.end());
//枚举a
for(int i=0;i<n;i++){
//减少重复枚举
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int k=n-1;
int target = -nums[i];
for (int j = i + 1; j < n; ++j) {
// 需要和上一次枚举的数不相同
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
// 需要保证 b 的指针在 c 的指针的左侧
while (j < k && nums[j] + nums[k] > target) {
--k;
}
// 如果指针重合,随着 b 后续的增加
// 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
if (j == k) {
break;
}
if (nums[j] + nums[k] == target) {
answer.push_back({nums[i], nums[j], nums[k]});
}
}
}
return answer;
}
};