3.给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
- 示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 - 示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 - 示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
拿到这个题目首先的想法是建立一个滑动窗口,利用两个变量来维护,之后对字符串进行遍历,一边遍历一边对滑动窗口内的所有字符进行检测是否重复,如果没有重复则窗口右侧增加一位,如果有重复,则滑动窗口左侧指针更新为窗口内重复元素的位置+1。代码如下:
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
int main(int argc, char const *argv[])
{
string s = "pwwkew"; //样例信息
int l, ans; //利用l和i维持一个滑动窗口,窗口范围是[l,i)
l = 0;
ans = 0;
string s_temp;
for (int i = 0; i < s.length(); ++i)
{
s_temp = s.substr(l,i-l); // s.substr(pos,n); 从第pos个位置开始(包括pos),返回n个字符;
for(int j = 0; j < s_temp.length(); j++){ //从滑动窗口内开始遍历,如果串口内存在与正在扫描的第i个字符相同,则改变l指针
if( (s_temp[j] == s[i]) && (i != 0) ){
l = l + j + 1; //把l指针位置改变为:在窗口内发现的重复字符的后一个。
}
}
if( (i - l + 1) >= ans ){ //更新最大滑动窗口长度;
ans = (i - l + 1);
}
}
};
}
cout << ans << endl;
return 0;
}
提交结果时间复杂度太高 (O(n^2)) ,后来学习了hash_map,可以利用hash—map来记录每个字符串出现的位置,这样就不需要对滑动窗口的每个元素进行遍历了,而且hash_map的复杂度是(O(1)),所以最后的复杂度是 (O(n)) 。代码更改如下:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int res = 0, left = -1, n = s.size();
unordered_map<int, int> m;
for (int i = 0; i < n; ++i) {
if (m.count(s[i]) && m[s[i]] > left)
left = m[s[i]];
m[s[i]] = i;
res = max(res, i - left);
}
return res;
}
};
5.最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
- 示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。 - 示例 2:
输入: "cbbd"
输出: "bb"
这个题目,也很简单,分两种情况依次遍历,分别找出奇数和偶数长度的子串就可以了,注意数组越界问题(找了挺长时间哪里数组越界,看来对编程还是不敏感),此处在search函数里left和right指针在进行判断之前,已经是不符合判断条件的状况了,所以我们如果后续会利用这两个变量,需要对其进行复原,即left++
和right--
(就是这里找了好久,(>_<)),代码如下:
class Solution {
public:
string longestPalindrome(string s) {
int n = s.length();
if(n< 2)
return s;
int start = 0;
int maxLen = 0;
for(int i = 0; i < s.length();i++) //寻找长度为奇数
search(s, i, i, start, maxLen);
for(int i = 0; i < s.length()-1;i++) //寻找长度为偶数
search(s, i, i+1, start, maxLen);
return s.substr(start, maxLen);
}
void search(string s,int left, int right, int& start,int& maxLen){
while( (left >= 0) && (right < s.length()) && (s[left] == s[right]) ){
left--;
right++;
}
left++;
right--;
if( (right - left + 1) > maxLen ){
maxLen = right - left + 1;
start = left;
}
}
};
今天尝试了用动态规划的方法去解题,动态规划是一个很像分治法的算法,都是“大事化小小事化了”的思想。动态规划实际上就是依次推这个公式:(F(n) = F(n-1)+F(n-2)),其中(F(n))使我们想要的最终状态,当然我们需要边界条件,就是(F(0)=a)和(F(1)=b),因为到了这里我们就不能继续利用上面的地推公式了,要在此收敛。当然我们可以正着推,也可以反过来递归求解,但是递归求解相当于对一颗二叉树的所有节点进行求解,故时间复杂度是(O(2^n)),所以一般从边界条件开始正着推,此时也可以利用备忘录算法,把已经得到的数值放到map或者数组里,避免重复求解。
这个题目里我利用了mark[i][j]
这个数组(我们只利用 i < j 的这一部分),i就是子串的左边界,j就是子串的右边界,如果(s[i,j])这个子串是一个回文子串那么mark[i][j] = 1
。判断其是否是回文子串的条件是动态规划算法的核心 if( (s[i] == s[j]) && ( j-i<2 || mark[i+1][j-1] ) )
,其边界条件是mark[i][i]=1
。除此之外,需要注意的是,因为我们是从边界条件开始递推所有数组的状态,所以我们需要沿着对角线方向依次向上遍历mark
数组,以确保递推时候的正确性,图示如下:
![](https://img-blog.csdnimg.cn/2019050615005332.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Nlbjg3MzU5MTc2OQ==,size_16,color_FFFFFF,t_70)
class Solution {
public:
string longestPalindrome(string s) {
int start = 0;
int n = s.length();
if (n < 2 )
return s;
int mark[n][n] = {0};
for(int i = 0; i < n ;i++) //初始化数组
for(int j = 0;j < n;j++)
mark[i][j]=0;
int MaxLen = 0;
int left = 0;
for(int k = 0; k < n; k++){
for(int i = 0,j = k; i < n - k; i++,j++){ //确保沿对角线向数组右上方遍历;
mark[i][i] = 1;
if( (s[i] == s[j]) && ( ( j-i<2 ) || mark[i+1][j-1] ) )
mark[i][j] = 1;
if( mark[i][j] && (j-i+1 > MaxLen) ) {
MaxLen = j-i+1;
start = i;
}
}
}
return s.substr(start,MaxLen);
}
};
实际提交后,会发现这种算法的时间复杂度实际很不理想,这和遍历数组的方式有关。数组在计算机中是按行线性排列的,而且计算机在从内存或缓存加载数据的时候,会默认把当前访问的数据以及其附近的多组数据一同加载,这样可以提高下次访问的速度(计算机会猜测你下次极有可能访问临近的元素),然后我们的这种访问方式就使得计算机的这种优化无用了,所以我们每次访问数组中的元素,计算机都会从新加载数据,进而时间复杂度极其不理想。
刚才上网看还有一个马拉车算法,今天还有事情,明天看一看回来更!码住
6.Z字形变换
将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 "LEETCODEISHIRING" 行数为 3 时,排列如下:
L C I R
E T O E S I I G
E D H N
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"LCIRETOESIIGEDHN"。
请你实现这个将字符串进行指定行数变换的函数:
string convert(string s, int numRows);
- 示例 1:
输入: s = "LEETCODEISHIRING", numRows = 3
输出: "LCIRETOESIIGEDHN" - 示例 2:
输入: s = "LEETCODEISHIRING", numRows = 4
输出: "LDREOEIIECIHNTSG"
解释:
L D R
E O E I I
E C I H N
T S G
这个题目比较简单,很容易发现规律:对于每n排竖直排列的字母,相差的索引都是size = 2 * ,numRows - 2
个。随后我们判断是否是首行或者尾行,如果是,直接利用上面规律就好,如果不是,我们需要插入 s[j + size - 2 * i]
个元素(i是当前行数,j是一个循环变量变量,j每次递增size个单位)。此外需要注意数组是否越界,和输入空字符串的情况。
class Solution {
public:
string convert(string s, int numRows) {
if (numRows <= 1) return s;
string res = "";
int size = 2 * numRows - 2;
for (int i =0; i< numRows; i++){
for(int j = i; j < s.length(); j = j + size){
if( (i == 0)||(i==numRows-1) ){ //说明是首行或者尾行
res += s[j];
}else{ //中间行
res += s[j];
if( j + size - 2 * i < s.length() ) //边界判断
res += s[j + size - 2 * i];
}
}
}
return res;
}
};
12.整数转罗马数字
罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。
字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:
I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
给定一个整数,将其转为罗马数字。输入确保在 1 到 3999 的范围内。
-
示例 3:
输入: 9
输出: "IX" -
示例 4:
输入: 58
输出: "LVIII"
解释: L = 50, V = 5, III = 3. -
示例 5:
输入: 1994
输出: "MCMXCIV"
解释: M = 1000, CM = 900, XC = 90, IV = 4.
这个题目拿过来第一反应是要把每个位数分离出来,随后转换成罗马数字,再拼接到一起。中间利用了map:
sclass Solution {
public:
string intToRoman(int num) {
string ans = "";
int t = 10;
int temp = 0;
while( num != 0 ) {
temp = num % t;
num -= temp;
t *= 10;
ans = getOneRoman(temp) + ans;
}
return ans;
}
string getOneRoman(int num){
map<int,string> mapRom = {{1,"I"},{4,"IV"},{5,"V"},{9,"IX"},{10,"X"},{40,"XL"},{50,"L"}, {90,"XC"},{100,"C"}, {400,"CD"},{500,"D"},{900,"CM"}, {1000,"M"} };
map<int,string>::reverse_iterator it;
string OneStr = "";
while(num != 0){
for(it = mapRom.rbegin(); it != mapRom.rend(); it++ ) {//从后向前遍历
if(it->first <= num ){
num -= it->first;
OneStr += it->second;
break;
}
}
}
return OneStr;
}
};
提交之后意识到时间复杂度惨不忍睹(一个数字长度是n,那么这个方式(n^k))。。。。。。。
其实何必把每个位数单独分离出来呢?我们从大向小遍历,转换一个位数后,把基数减去相应的位数,继续在map中寻找。这样在map中也只需要遍历一遍。
class Solution {
public:
string intToRoman(int num) {
string ans = "";
vector<int> values={1000,900,500,400,100,90,50,40,10,9,5,4,1};
vector<string> strs={"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"};
int i = 0;
while( num != 0 || i > values.size() ){
if( num >= values[i]){
num -= values[i];
ans += strs[i];
}else
i++;
}
return ans;
}
};