总结:中级算法中的数组和字符串问题中依然沿用了初级算法问题中的双指针法,例如题目1;并且加入了一些数组中动态规划的问题,例如题目4,题目5,题目6。
题目1:三数之和
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
思路:三数之和与两数之和其实是一个类型的题目,两数之和时a+b==0,而三数之和是a+b+c==0,转化一下就是a+b==-c,实际上还是两数之和问题。先对数组进行排序,之后遍历数组,依次将第i个元素当做c,之后在第i+1个元素和第len-1个元素各放一个指针分别当做b和c。不停将a,b,c相加,若大于目标则将右指针向前推一位(因为排过序所以等于缩小二者的和),反之则将左指针向后推一位(增大二者的和),直到两指针相遇。重复上述过程直到遍历完所有数组内元素。但要注意一点,无论是a,b,c,都要跳过相同数字。
vector<vector<int> > threeSum(vector<int>& nums)
{
int len = nums.size();
vector<vector<int> > result;
if(len<3)
return result;
sort(nums.begin(),nums.end());
for(int i=0;i<len;i++)
{
//跳过相同的数字
if(i>0&&nums[i]==nums[i-1])
continue;
int point1 = i+1;
int point2 = len-1;
while(point1<point2)
{
int sum = nums[i]+nums[point1]+nums[point2];
if(sum==0)
{
vector<int> list{nums[i],nums[point1],nums[point2]};
result.push_back(list);
point1++;
//跳过相同的数字
while((point1<point2)&&(nums[point1]==nums[point1-1]))
point1++;
point2--;
//跳过相同的数字
while((point1<point2)&&(nums[point2]==nums[point2+1]))
point2--;
}
else if(sum<0)
{
point1++;
//跳过相同的数字
while((point1<point2)&&(nums[point1]==nums[point1-1]))
point1++;
}
else if(sum>0)
{
point2--;
//跳过相同的数字
while((point1<point2)&&(nums[point2]==nums[point2+1]))
point2--;
}
}
}
return result;
}
题目2:矩阵置零
给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。
示例 1:
输入:
[
[1,1,1],
[1,0,1],
[1,1,1]
]
输出:
[
[1,0,1],
[0,0,0],
[1,0,1]
]
示例 2:
输入:
[
[0,1,2,0],
[3,4,5,2],
[1,3,1,5]
]
输出:
[
[0,0,0,0],
[0,4,5,0],
[0,3,1,0]
]
思路:遍历二维数组,分别用两个一维数组记录二维数组需要变成0的行和列,之后分别遍历这两个数组将对应行和列化为0。
void setZeroes(vector<vector<int> >& matrix)
{
int m = matrix.size();
if(m==0)
return;
int n = matrix[0].size();
//记录有0的行
int row[m];
memset(row,0,sizeof(int)*m);
//记录有0的列
int col[n];
memset(col,0,sizeof(int)*n);
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(matrix[i][j]==0)
{
if(row[i]==0)
row[i] = 1;
if(col[j]==0)
col[j] = 1;
}
}
}
for(int i=0;i<m;i++)
{
if(row[i]==1)
{
for(int j=0;j<n;j++)
matrix[i][j] = 0;
}
}
for(int i=0;i<n;i++)
{
if(col[i]==1)
{
for(int j=0;j<m;j++)
matrix[j][i] = 0;
}
}
}
题目3:字谜分组
给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
示例:
输入: ["eat", "tea", "tan", "ate", "nat", "bat"],
输出:
[
["ate","eat","tea"],
["nat","tan"],
["bat"]
]
思路:遍历数组,用一个flag数组记录数组中元素是否已经被分组。当遍历到i元素,先将i元素按字母分解,将每个字母出现的次数存储到一个长度为26的数组arr中(代表26个字母),之后遍历其后未被分组的元素,按相同方法进行拆解并保存到另一个长度为26的数组temp中,之后对两个数组进行比较,若各字母出现次数均相同则为其分组,并标记已分组。重复上述过程直到所有元素均被分组。
vector<vector<string> > groupAnagrams(vector<string>& strs)
{
int len = strs.size();
vector<vector<string> > result;
if(len==0)
return result;
int flag[len];
memset(flag,0,len*sizeof(int));
int arr[26];
int temp[26];
for(int i=0;i<len;i++)
{
if(flag[i]==0)
{
vector<string> list;
list.push_back(strs[i]);
memset(arr,0,26*sizeof(int));
int s1_len = strs[i].size();
//拆解原始字符串的字母
for(int j=0;j<s1_len;j++)
{
int index = strs[i][j]-'a';
arr[index] += 1;
}
//与原始字符串进行比较
for(int j=i+1;j<len;j++)
{
if(flag[j]==0)
{
int s2_len = strs[j].size();
if(s2_len!=s1_len)
continue;
memset(temp,0,26*sizeof(int));
int k = 0;
//分解被比较字符串
for(k=0;k<s2_len;k++)
{
int index = strs[j][k]-'a';
temp[index] += 1;
}
//比较
for(k=0;k<26;k++)
if(temp[k]!=arr[k])
break;
if(k==26)
{
list.push_back(strs[j]);
flag[j] = 1;
}
}
}
result.push_back(list);
}
}
return result;
}
题目4:无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
思路:使用动态规划,用dp[i]记录以i元素结尾的最长子串长度。遍历字串,当遍历到元素s[i],首先计算s[i]与其上次出现时相隔的字母数count,一.如果count等于0,那么说明该元素之前从没出现过,那么此时以i元素结尾的最长子串长度就相当于在i-1元素结尾的最长子串长度上加1,此时的状态转移方程是dp[i]=dp[i-1]+1;二.如果count不等于0,那么此时就要判断一下,如果count>dp[i-1],那么说明s[i]上次出现的位置不在以i-1元素结尾的最长字串内,那么此时以i元素结尾的最长子串就是以i-1元素结尾的最长字串+s[i],状态转移方程同样为dp[i]=dp[i-1]+1;如果count<dp[i-1],那么说明s[i]上次出现的位置在以i-1元素结尾的最长字串内,那么此时以i元素结尾的最长子串就是s[i]与其上次出现时相隔的这段子串,此时状态转移方程为dp[i]=count。
int lengthOfLongestSubstring(string s)
{
int len = s.size();
if(len<=1)
return len==0?0:1;
//以i元素结尾的最长子串长度
int dp[len];
memset(dp,0,len*sizeof(int));
//记录每个字母上次出现的位置
dp[0] = 1;
int max = 1;
//字符距其上次出现位置的距离
int count = 0;
for(int i=1;i<len;i++)
{
count = 0;
//这段处理可以用map做记录来简化算法
for(int j=i-1;j>=0;j--)
{
if(s[i]==s[j])
{
count = i-j;
break;
}
}
//count等于0意味着这个字符没有出现过
if(count!=0)
{
if(count>dp[i-1])
dp[i] = dp[i-1]+1;
else
dp[i] = count;
}
else
dp[i] = dp[i-1]+1;
max = max>dp[i]?max:dp[i];
}
return max;
}
题目5:最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
思路:使用动态规划,用一个二维数组dp[i][j]记录从i到j的子串是否回文。以为单个字母肯定回文,所以先将单个字母dp[i][i]全部记录为回文子串,之后判断字符串各个子串是否为回文子串。当s[i]==s[j]时,若其长度小于3,那么它一定是回文子串,若其长度大于3,若s[i+1]到s[j-1]形成的子串为回文子串,则s[i]到s[j]形成的子串也为回文子串。
string longestPalindrome(string s)
{
int len = s.size();
if(len<=1)
return s;
//最长子串的起始下标和长度
int start = 0;
int length = 1;
//用于表示从i到j的子串是否回文
int dp[len][len];
memset(dp,0,sizeof(dp));
for(int i=0;i<len;i++)
dp[i][i] = 1;
//在判断dp[i][j]之前必须保证dp[i+1][j-1]被判断过了
//所以要采取i降序j升序的遍历法
for(int i=len-2;i>=0;i--)
{
for(int j=i+1;j<len;j++)
{
if(s[i]==s[j]&&(j-i<3||dp[i+1][j-1]==1))
{
dp[i][j] = 1;
if(j-i+1>length)
{
start = i;
length = j-i+1;
}
}
}
}
return s.substr(start,length);
}
题目6:递增的三元子序列
给定一个未排序的数组,判断这个数组中是否存在长度为 3 的递增子序列。
数学表达式如下:
如果存在这样的 i, j, k, 且满足 0 ≤ i < j < k ≤ n-1,
使得 arr[i] < arr[j] < arr[k] ,返回 true ; 否则返回 false 。
说明: 要求算法的时间复杂度为 O(n),空间复杂度为 O(1) 。
示例 1:
输入: [1,2,3,4,5]
输出: true
示例 2:
输入: [5,4,3,2,1]
输出: false
思路:遍历数组,用一个数组dp记录i元素之前比其小元素的个数。当遍历到第i个元素时,向前依次遍历i之前元素j,若i元素大于j元素,并且dp[j]大于1,这说明j元素之前比其小的元素个数大于1,而i又大于j,此时形成了一个递增三元序列。
bool increasingTriplet(vector<int>& nums)
{
int len = nums.size();
if(len<3)
return false;
//记录i元素之前比其小的数的个数
int dp[len];
memset(dp,0,len*sizeof(int));
for(int i=1;i<len;i++)
{
for(int j=i-1;j>=0;j--)
{
if(nums[j]<nums[i])
{
if(dp[j]>=1)
return true;
else
dp[i] += 1;
}
}
}
return false;
}