不管做什么类型的题,当然都是从简单的开始😢
给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。
注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。
示例 :
输入:nums = [3,30,34,5,9]
输出:"9534330"
来源:力扣(LeetCode)
题解
具体思路来自这位大佬:(个人只修改了一点点,基本没差别)
https://leetcode-cn.com/problems/largest-number/solution/bao-ni-miao-dong-de-chao-jian-dan-shi-xi-sys4/
class Solution {
public:
string largestNumber(vector<int>& nums) {
vector<string> buf;
buf.reserve(nums.size());//预先分配空间
for(int i : nums)
{//把int转换成string并添加进buf
buf.emplace_back(to_string(i));
}
//本题重点,sort的自定义排序,第三个参数就是自定义的规则
sort(buf.begin() , buf.end() , [](string&a , string&b){return a+b > b+a;});
if(buf[0] == "0") return "0";
string res;
for(string i : buf)
{//拼接字符串
res+=i;
}
return res;//返回结果
}
};
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 :
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
来源:力扣(LeetCode)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int N = s.size();
if (N ==0) return 0;//特殊情况
if (N == 1) return 1;//特殊情况
unordered_map<char, int> map;
int L = 0 , max = 0;//L左指针,max是最大长度
for(int R = 0;R<N;R++)//R是右指针
{
//查找当前R所指的字符是否出现过
if(map.find(s[R]) != map.end())
//这里的意思是把L的值移动到重复的数字右边,因为我们用map记录了下标
//所以+1就可以到重复字符的右边了,这个比较是为了防止L左移
{ L = L>map[s[R]] + 1?L:map[s[R]] + 1; }
// 记录当前字符的下标,会更新的,所以写在if外边
map[s[R]] = R;
//+1是因为L和R是闭区间
max = max>(R-L+1)?max:(R-L+1);
}
return max;
}
};
415. 字符串相加
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。用字符串返回
提示:
num1 和num2 的长度都小于 5100
num1 和num2 都只包含数字 0-9
num1 和num2 都不包含任何前导零
你不能使用任何內建 BigInteger 库, 也不能直接将输入的字符串转换为整数形式
来源:力扣(LeetCode)
不能直接转,就算直接转了,也会因为数据过大而溢出,各种bug,所以考虑用上次写链表数字相加一样的方法:
缺位补0,当然了,结果是反的,所以最后用reverse反转我们的结果string
联动:
题目
文章
class Solution {
public:
string addStrings(string num1, string num2) {
int N1 = num1.size()-1 , N2 = num2.size()-1;
string res;
int sum = 0 , carry = 0;
while(N1 >=0 || N2 >= 0)
{
char n1 = N1>=0?num1[N1]:'0';
char n2 = N2>=0?num2[N2]:'0';
sum = (n1-'0') + (n2-'0') + carry;
carry = sum/10;
res += sum%10 + '0';
N1--;N2--;
}
if(carry > 0) res += carry + '0';
reverse(res.begin(),res.end());
return res;
}
};
剑指 Offer 50. 第一个只出现一次的字符
第一眼看见题目就想到哈希表,第一次遍历时将所有元素存入哈希表,并且每出现一次,key对应的data就+1,于是,只出现一次的字符的data就是1,
然后第二次遍历字符串,遇见data为1就输出,如果遍历完了都找不到,就说明没有不重复的字符,所以输出空格 ’ ’
class Solution {
public:
char firstUniqChar(string s) {
unordered_map<char , int> map;
for(int i = 0;i<s.size();++i)
{
++map[s[i]];
}
for(int i = 0;i<s.size();++i)
{
if(map[s[i]] == 1)
return s[i];
}
return (char)' ';
}
};
实现 strStr() 函数。
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
来源:力扣(LeetCode)
重点来了,字符串匹配,我除了暴力法以外,也只知道一个kmp了,于是就用kmp来吧
kmp算法的重点就是next数组了,算出了next数组,整个算法也差不多完成了
那next数组是什么呢,其实就是一个字符串里各个长度的公共前后缀
如下图,ababaaaba的next数组就是【001231123】
当然也可以把数组里的数字都减个1 , 就变成了 【-1-10120012】
那么怎么用代码来实现呢?
next数组
(这里实现的是不减1的 , 减1版本的只需要做小小的修改,会在代码里标出来)
void getNext(string& s , vector<int>&next)
{
j = 0;//j = -1;
next[0] = 0;//next[0] = -1;
//这里记得初始化i为1,因为next[0]我们已经初始化过了
for(int i = 1;i<s.size();i++)
{
while(j > 0 && s[j] != s[i])//j > 0 —— j >= 0
{//如果不匹配,就回溯j的位置直到匹配或者j到达初始位置
j = next[j-1];// j = next[j];
}
//如果匹配,j就往右走,看是不是有更长的公共前后缀
if(s[j] == s[i])//if(s[j+1] == s[i])
{
j++;
}
next[i] = j;//存放公共前后缀的长度到对应的位置
}
}
为什么说算出了next数组,整个算法也差不多完成了?
因为主程序里的利用next数组匹配的算法思路和代码也是和
算next数组时差不多的
int strStr(string haystack, string needle)
{
vector<int> next;
next.resize(needle.size());
j = 0;//-1
getNext(needle,next);//算出next数组
//下面的i要初始化为0,因为要从头开始匹配
for(int i = 0;i<haystack.size();i++)
{
while(j > 0 && haystack[i] != needle[j])//j > 0 —— j >= 0
{//回溯
j = next[j-1];// j = next[j];
}
if(haystack[i] == needle[j])//if(haystack[i] == needle[j+1])
{//前进
j++;
}
//完美匹配,就返回位置
if(j == needle.size())//if(j == needle.size()-1)
{
return (i - needle.size() + 1);
}
}
return -1;//如果上面没返回,就说明没用匹配到,返回-1;
}
可以发现,都是一个for遍历字符串
然后while来回溯不匹配的情况
if来前进
只是细节有一点不同,框架都是一样的
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 “”。
示例 1:
输入:strs = [“flower”,“flow”,“flight”]
输出:“fl”
示例 2:
输入:strs = [“dog”,“racecar”,“car”]
输出:""
解释:输入不存在公共前缀。
来源:力扣(LeetCode)
思路:
纵向扫描,遍历每一个字符串,依次比对每一个字符串对应位置的字符,看有没有不相同的,如果有不相同的,就用string::substr提取前几位然后返回
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
int size = strs.size();//获取字符串数量
if(size == 0) return "";//如果是空的就返回空字符串
int length = strs[0].size();//获取第一个字符串的长度
//这里其实获取谁的都可以,但是第一个比较方便,
//我们就用第一个字符串作为基准取进行匹配
for(int i = 0;i<length;i++)//遍历第一个字符串
{
char ch = strs[0][i];//提取第一个字符串的对应字符
for(int j = 1;j<size;++j)//遍历所有字符串的对应字符
{
if(strs[j][i] != ch || i == strs[j].size())
{//找到最大公共前缀了,提取,返回
return strs[0].substr(0,i);
}
}
}
//这里是正常情况退出循环,即strs[0]都被我们遍历完了,
//发现strs[0]里的每一个字符都与其他字符串的对应位置匹配,
//那咋办,直接返回呗!
return strs[0];
}
};
1190. 反转每对括号间的子串
来个简单的,放松一下😘
给出一个字符串 s(仅含有小写英文字母和括号)。
请你按照从括号内到外的顺序,逐层反转每对匹配括号中的字符串,并返回最终的结果。
注意,您的结果中 不应 包含任何括号。
示例 1:
输入:s = “(abcd)”
输出:“dcba”
示例 2:
输入:s = “(u(love)i)”
输出:“iloveu”
来源:力扣(LeetCode)
思路:用栈和队列来实现,不知道为什么,一看见反转,我就会想到栈
class Solution {
public:
string reverseParentheses(string s) {
stack<char> st;
queue<char> buf;
for(char ch : s)
{
if(ch != ')')//不是右括号就压入栈
st.push(ch);
else
{//如果是右括号,就放入队列,再放回来
while(st.top()!='(')
{//放入队列
buf.push(st.top());
st.pop();
}
st.pop();//删掉多余的左括号
while(!buf.empty())
{//放回栈内
st.push(buf.front());
buf.pop();
}
}
}
string res;
res.resize(st.size());//这里不能忘,不然无法用下标访问
//从后往前给新字符串赋值
for(int i = res.size()-1;i>=0;i--)
{
res[i] = st.top();
st.pop();
}
return res;
}
};
将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 “PAYPALISHIRING” 行数为 3 时,排列如下:
P A H N
A P L S I I G
Y I R
来源:力扣(LeetCode)
一开始我还没搞懂什么叫z字变换,因为示例里只给除了3行的情况,其实结合一下4行的情况看一下就很好理解了
又根据题目要求,变换后的字符串是一行一行读取的,所以中间没必要留空,如下图,直接放到后面就行了
所以,我们就不用考虑横向的变化了,只考虑上下的变化就好,用一个bool型变量flag来决定往下走还是往上走;
说一下特殊情况:
1:
指定行数为1,这样的话,反正s也只会复制一份,为了避免中间不必要的运算,直接返回s
2:
行数小于等于字符串长度,这样的话,每一行最多也就一个字符,拼接完成后还是s,所以直接返回s
class Solution {
public:
string convert(string s, int numRows) {
int N = s.size();
//解决特殊情况
if(numRows == 1 || N<=numRows) return s;
int row = numRows, cur = 0;
bool flag = false;
vector<string>buf(row);
for(char ch : s)
{
buf[cur] = buf[cur]+ch;
if(cur == 0) flag =true;
else if(cur == row-1) flag = false;
cur = cur+(flag?1:-1);
}
string res;
//拼接,用引用节省空间提高效率
for(string&temp : buf)
res += temp;
return res;
}
};
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
来源:力扣(LeetCode)
这一题挺有意思的,一开始没反应过来,还想用双指针的,后来发现这就是个匹配括号的题目而已,一下就清晰了,用栈即可,而且
C++的string也支持类似栈的操作,top就用back , pop就是pop_back , push就是push_back甚至可以直接用+法,不过为了看上去统一一点,这里还是用push_back
代码:
class Solution {
public:
string removeDuplicates(string S) {
string res;
for(char ch : S)
{
if(res.empty() || ch != res.back()) res.push_back(ch);
else if(ch == res.back() && !res.empty()) res.pop_back();
}
return res;
}
};
给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。
示例 1:
输入: num1 = “2”, num2 = “3”
输出: “6”
示例 2:
输入: num1 = “123”, num2 = “456”
输出: “56088”
来源:力扣(LeetCode)
思路:
其实就是用平时做乘法的思路
我们能发现,三位乘三位,尽管是最大值的999*999,他们的乘积也没有大于6位,其实所有的乘积都一样,满足这个规律,然后为了运算方便,我们用一个数组buf [ m+n ]储存中间值,其实用字符串也可以,但是运算还得用数字的逻辑,转来转去太麻烦,我就偷懒用了数组,其实用字符串的话,最后删掉多余的0就可以返回了,比较节省空间
常规乘法的思路,是把所有乘积算出来一起加,代码里为了节省空间,只能算出一个加一个了buf就是存放这些值的缓冲数组
class Solution {
public:
string multiply(string num1, string num2) {
if(num1 == "0" || num2 == "0") return "0";
int n1 = num1.size(), n2 = num2.size();
vector<int> buf(n1+n2,0);
for(int i = n1-1;i>=0;--i)
{
for(int j = n2-1;j>=0;--j)
{//这里注意前两句,都得是 += ,不然就没效果了,别忘了乘法的思路
buf[i+j+1] += (num1[i]-'0')*(num2[j]-'0');//先保存完整数字
buf[i+j] += buf[i+j+1]/10;//给前面一位赋值为进位
buf[i+j+1] %= 10;//保留个位
}
}
string res;
int i = 0;
while(buf[i] == 0) i++;//跳过开头多余的0
while(i< buf.size()) res += (char)(buf[i++]+'0');
return res;
}
};
给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
示例:
输入: [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出:
[
[“ate”,“eat”,“tea”],
[“nat”,“tan”],
[“bat”]
]
来源:力扣(LeetCode)
思路:
哈希表+排序,如何判断两个字符串是不是字母异位词?这就得用上排序了,很明显我们知道"acb" 和 "bac"是字母异位词,然后他们排序后的字符串都是"abc"我们就用这个作为哈希表的key,原字符串作为值,因为可以有多个值,所以值用字符串数组来填入所有满足条件的字符串,
遍历原字符串数组,每次遍历都用一个临时字符串存放排序过后的字符串,然后在哈希表里查找是否有这个key,如果没有就建立,有的话就在对应的值里添加被我们遍历到的字符串
遍历完以后,所有字符串都被我们分好组了,最后按组把字符串填入结果二维数组就行了
代码:
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string,vector<string>>map;
for(string&temp:strs)//for循环遍历字符串数组
{
string buf = temp;//存放排序后的字符串
sort(buf.begin(),buf.end());
if(map.find(buf)!=map.end())//如果已经这样的组了,就加上去
{
map[buf].emplace_back(temp);
}
else
{//如果没有找到这样的组,就建立一个字符串数组,并把当前遍历到的字符串填进去
vector<string>tstr;
tstr.emplace_back(temp);
map[buf] = tstr;
}
}
vector<vector<string>>res;
res.reserve(map.size());
for(auto& temp:map)
{//将哈希表里的字符串数组填入结果二维数组
res.emplace_back(temp.second);
}
return res;
}
};
其实这里还可以优化一下,就是哈希表的的值,可以用字符串数组的下标,也就是从vector< string >换成vector< int >,这样在最后填入结果数组的时候会稍微麻烦一点,但是能节约一点内存的使用,并且不会有时间上损失,甚至更快了,因为不用重复的在内存中复制字符串
代码:
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string,vector<int>>map;
for(int i = 0;i<strs.size();++i)
{
string& temp = strs[i];
string buf = temp;
sort(buf.begin(),buf.end());
if(map.find(buf)!=map.end())
map[buf].emplace_back(i);
else
map[buf] = vector<int>{i};
}
vector<vector<string>>res(map.size());
int count = 0;//这个
for(auto& temp:map)
{
for(int i = 0;i<temp.second.size();++i)//这个for是给二维数组里的一维数组赋值
{//这里还可以再优化一下,先给一维数组一个确定的大小,既能节省空间,也能节省时间
res[count].emplace_back(strs[temp.second[i]]);
}
++count;//这个count决定给哪个一维数组赋值
}
return res;
}
};
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。
示例 1:
输入: “the sky is blue”
输出: “blue is sky the”
规则:
无空格字符构成一个单词。
输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
来源:力扣(LeetCode)
思路就是用双指针把有效的单词给提取出来,从后往前拼在一起并在中间添加一个空格
拼接可以用字符串数组缓冲,也可以直接加在结果字符串上,按遍历顺序不同,推荐使用的方法也不同
思路很简单,实现的方法也很多
我一开始用的不是双指针,而是直接遍历+数组缓冲,但是这样会多提取几个无效的空字符串"",得在拼接的时候判断是否进行拼接
后来就用了双指针,在一次遍历中用双指针指向每一个有效的单词,这样可以有效的避免没必要的提取,然后我没用数组缓冲,直接对结果字符串进行加法运算,并且提取的的时候用的是substr提取目标字符串的子串,最后发现内存的使用差的离谱,我就猜是不是因为重复的对结果字符串进行加法的关系,在内存中疯狂的进行无谓的复制
然后我改成了字符串数组+提取子串的方法,内存占用大大降低
最后一步,我把提取子串的方式,改成了一个个字符的添加
速度也有了一点提升,都是在4ms和8ms之间,不像提取子串,都是8ms和12ms
但是内存占用排名还是很低,而且代码略显繁琐,如下
代码:
class Solution {
public:
string reverseWords(string s) {
string res;
string temp;
vector<string>buf;
int L = 0; while(s[L]==' ')++L;//跳过前缀0
int R = L;
while(L<s.size())
{
if(s[R]!=' ' && R < s.size())
{
temp.push_back(s[R]);//这是单个字符的添加进temp(二选一)
++R;
}
else
{
//temp = s.substr(L,R-L);//这是提取子串的添加进temp
buf.emplace_back(temp);//用数组缓冲,推荐
//res = temp+' '+res;//用加法,不推荐
temp.clear();
while(s[R] ==' ' && R<s.size())++R;
L = R;
}
}
for(int idx = buf.size()-1;idx>=0;idx--)//从后往前,把单词添加进结果字符串
res+=buf[idx]+' ';//在每个单词后面加上一个空格
res.pop_back();//删除结果后方多余的空格
return res;
}
};
最后,我还是忍不住看了大佬们的题解,终于恍然大悟,终于明白了为什么做加法效率那么低
最主要的还是遍历的顺序
从前往后的话,因为顺序的关系 我的res在做加法的时候不能使用 +=
字符串的+=效率很高,res = res + temp 和 res += temp效率差的不是一点两点,看图
所以才导致了那种局面,但是如果从后往前遍历得话,就可以用+=了,完全不用数组缓冲,虽然效率没高多少,但是代码简洁了很多!
代码:
class Solution {
public:
string reverseWords(string s) {
int n = s.size(), L, R = n - 1;
string res;
while(R >= 0){
while(R >= 0 && s[R] == ' ') --R;
if(R < 0) break;
for(L = R; L >= 0 && s[L] != ' '; --L);
res += (s.substr(L + 1, R - L) + " ");
R = L;
}
res.pop_back();
return res;
}
};