编程总结
每每刷完一道题后,其思想和精妙之处没有地方记录,本篇博客用以记录刷题过程中的遇到的算法和技巧
双指针一定要想好思路先,切记先陷入到题里面去了,难以拔出来。
多练习吧
双指针尤其要注意用例出现超时;
切记直接暴力两层循环解题,能用左右指针使用双指针解题。
手法是:
- 初始化 某个[i, j)区间
- 保持 某个[i,j)区间
- 终止 找终止条件
双指针的三大步骤:初始化,保持,终止牢记 牢记
最长的指定瑕疵度的元音子串
定义:开头和结尾都是元音字母(aeiouAEIOU)的字符串为 元音字符串 ,其中混杂的非元音字母数量为其 瑕疵度 。比如:
“a” 、 “aa”是元音字符串,其瑕疵度都为0
“aiur”不是元音字符串(结尾不是元音字符)
“abira”是元音字符串,其瑕疵度为2
给定一个字符串,请找出指定瑕疵度的最长元音字符子串,并输出其长度,如果找不到满足条件的元音字符子串,输出0。
子串:字符串中任意个连续的字符组成的子序列称为该字符串的子串
思路:非常经典的双指针玩法, 稍有不慎容易陷入思维黑洞无法自拔…理清思路
- 右移窗口的过程中,遇到非元音字母,瑕疵度增加
- 右移导致瑕疵度过大,此时左侧窗口收缩直到重新满足瑕疵度
- 当前窗口满足头尾元音,瑕疵度要求的条件
输入样例 3 复制
1
aabeebuu
输出样例 3
5
提示样例 3
满足条件的最长元音字符子串有两个,分别为aabee和eebuu,长度为5
int GetLongestFlawedVowelSubstrLen(int flaw, char *str)
{
int i;
char vowel[NUM_10] = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'};
int vowelNum[128] = {0};
int l = 0;
int r = 0;
int len = strlen(str);
int flawValue = 0;
int maxLen = 0;
// 计算元音字母
for (i = 0; i < NUM_10; i++) {
vowelNum[(vowel[i] - 'A')] = 1;
}
while (r < len) {
if (vowelNum[str[r] - 'A'] == 0) {
flawValue++; // 右移窗口的过程中,遇到非元音字母,瑕疵度增加
}
while (flawValue > flaw) { // 右移导致瑕疵度过大,此时左侧窗口收缩直到重新满足瑕疵度
if (vowelNum[str[l] - 'A'] == 0) {
flawValue--;
}
l++;
}
// 当前窗口满足头尾元音,瑕疵度要求的条件
if ((vowelNum[str[r] - 'A'] == 1) && (vowelNum[str[l] - 'A'] == 1) && (flawValue == flaw)) {
maxLen = fmax(r - l + 1, maxLen);
}
r++;
}
return maxLen;
}
26.删除有序数组中的重复项
思路,这题在空间不做要求的情况下可以使用桶排序,下面介绍 O(1) 空间复杂度的解法(已知是有序数组):
- 初始化:为了保证区间 [0…j) (注意这里是左闭右开区间)里没有重复元素。初始的时候,i = 0 ,下标为 0 的位置只有一个数,区间 [0…j) 一定不会出现重复,这件事情表示为区间 [0…0]
没有重复数字,即区间 [0…1) 没有重复数字,因此 j 初始化的时候需要等于 1。- 保持:遇到和 pre 指向的数字相等元素,i++ 直接看到下一个元素,如果 nums[i] != pre ,表示程序看到了第 1 个不重复的数字,此时需要赋值 nums[j] = nums[i] 和 pre = nums[j] ,然后让 j++ 指向下一个需要赋值的下标;
- 终止:循环结束以后 i = len ,程序看完了输入数组的所有元素,此时区间 [0…j) 里没有重复元素,它的长度为 j ,返回。
// Return :j 返回删除后数组的新长度
int removeDuplicates(int *nums, int numsSize)
{
int len = numsSize;
if (len < 2) {
return len;
}
int j = 1;
int pre = nums[0];
for (int i = 1; i < len; i++) {
if (nums[i] != pre) {
nums[j] = nums[i];
pre = nums[j];
j++;
}
}
return j;
}
674. 最长连续递增序列
思路:像这种带有最长***(最长元音,最长递增序列等),很有可能就可以使用双指针来解题,套用双指针三大手法即可。
int findLengthOfLCIS(int *nums, int numsSize)
{
int i = 0;
int j = 0;
int res = 1;
int result = 0;
if (numsSize < 2) {
return numsSize;
}
// 1.初始化[0, 1)严格单调递增
// 2.保持 [i,j) 循环不变量 [i...j) 严格单调递增
// 3.判断结束条件 j < numsSize.
for (j = 1; j < numsSize; j++) {
if (nums[j] <= nums[i]) { // 非单调递增序列
res = j - i; // res表示递增的元素个数
i = j; // 更新i
result = fmax(res, result);
res = 1;
}
else { // 单调递增序列
nums[i] = nums[j];
res++;
result = fmax(res, result);
}
}
return result;
}
27. 移除元素
int removeElement(int *nums, int numsSize, int val)
{
int j = 0;
int i = 0;
// 循环不变量: [i, j)不存在val的值.
for (j = 0; j < numsSize; j++) {
if (nums[j] != val) {
nums[i] = nums[j];
i++;
}
}
return i;
}
求差值的组合
给定一个数组,每个元素的值是唯一的,找出其中两个元素相减等于给定差值 diff 的所有不同组合的个数。
组合是无序的:如:(1, 4)和(4, 1)表示的是同一个组合。
样例
输入样例 1 复制
3
5
1 3 2 5 4
输出样例 1
2
提示样例 1
数组为[1 3 2 5 4], 差值 diff 为 3,其中 4 - 1 = 3,5 - 2 = 3,共 2 个组合满足条件,因此输出 2.
思路1:暴力遍历两两的差值
int Proc1(int *arr, int arrLen, int diff) // 暴力求解,两层循环,用例出现超时
{
int i, j;
int res = 0;
int resTmp;
qsort(arr, arrLen, sizeof(int), cmp);
diff = fabs(diff);
for (i = 0; i < arrLen; i++) {
for (j = i + 1; j < arrLen; j++) {
resTmp = fabs(arr[j] - arr[i]);
if (resTmp == diff) {
res++;
if (arr[j + 1] > arr[j]) { // 剪枝, 并没有用
break;
}
}
}
}
return res;
}
思路2:先做排序,一次O(n)遍历完就能找到满足条件的cnt. 有点点像二分查找那个套路,其实也可以使用二分查找,更快找到。
int Proc(int *arr, int arrLen, int diff) // 利用双指针求解,通过!
{
int i, j;
int resTmp;
qsort(arr, arrLen, sizeof(int), cmp);
diff = fabs(diff);
int r = 0;
int l = 0;
int cnt = 0;
while (r < arrLen) {
if (r == l) {
r++;
continue;
}
if (arr[r] - arr[l] < diff) {
r++;
} else if (arr[r] - arr[l] == diff) {
cnt++;
r++;
} else {
while (arr[r] - arr[l] > diff && l < r) {
l++;
}
}
}
return cnt;
}
3. 无重复字符的最长子串
思路:又看到了“最长”的字样,还是借鉴三步走,想清楚三步走后,基本就没有什么阻塞了。
- 初始化某个[i, j)区间
- 保持某个[i, j)区间
- 终止找终止条件
int JudgeStr(int j, int *str, char *s)
{
int res = 1;
if (str[s[j]] == 1) {
res = 0;
return res;
}
return res;
}
// 1. 初始化某个[i, j)区间
// 2. 保持某个[i, j)区间
// 3. 终止找终止条件
int lengthOfLongestSubstring(char *s)
{
int i = 0;
int j = 0;
int len = strlen(s);
int str[512] = { 0 };
int res = 0;
int step = 0;
while (i < len && j < len) {
if (JudgeStr(j, str, s) == 1) {
str[s[j]] = 1;
step++;
j++;
res = fmax(res, step);
}
else {
res = fmax(res, step);
i++;
j = i;
memset(str, 0, 512 *sizeof(int)); // 优化:耗时太严重,且又需要重新都计算一次
step = 0;
}
}
return res;
}
提交完效率有些低:
优化:
思路与上面类似,但区别是不需要清除掉 str 数组,出现重复字符,就左移窗口剔除左边的元素,直到满足 [i, j]区间都是不重复字符,此时窗口大小为 (j - i + 1).
也是三步走的思路,维持的区间为 [i, j]
int lengthOfLongestSubstring(char *s)
{
int len = strlen(s);
if (len == 0) {
return 0;
}
int str[128] = { 0 };
int i, j;
int res = 0;
for (i = 0, j = 0; j < len; j++) {
str[s[j]]++;
while (str[s[j]] > 1) {
str[s[i]]--; // 出现重复字符串,左移窗口,好手法!它这里是一直维持着前面计算的结果
i++;
}
res = fmax(res, j - i + 1); // (j - i + 1)为维持的窗口大小.
}
return res;
}
567. 字符串的排列
排列的含义:
首先,介绍一下排列!数学中的排列是:从n个不同元素中,任取m(m≤n)个元素(被取出的元素各不相同),按照一定的顺序排成一列,叫做从n个不同元素中取出m个元素的一个排列。
大家是不是觉得挺难懂的?好吧!咱用一个简单的例子说明:有1、2、3、4这四个数字,现在请列举出由这四个数字组成的四位数有几种排列?(注意数字不能重复)
直接求排列不好求,这里有个手法:
当 pFrep 和 winFreq 频数和种类都相等时,排列就满足.
// 判断排列是否满足,前提已经 cnt1 和 cnt2 字符长度都是n,只需要比较频次是否相等
bool equals(int *cnt1, int *cnt2) {
for (int i = 0; i < 26; i++) {
if (cnt1[i] != cnt2[i]) {
return false;
}
}
return true;
}
bool checkInclusion(char *s1, char *s2) {
int n = strlen(s1), m = strlen(s2);
if (n > m) {
return false;
}
int cnt1[26], cnt2[26];
memset(cnt1, 0, sizeof(cnt1));
memset(cnt2, 0, sizeof(cnt2));
// 填充好 n 区间大小的起始值
for (int i = 0; i < n; ++i) {
++cnt1[s1[i] - 'a'];
++cnt2[s2[i] - 'a'];
}
if (equals(cnt1, cnt2)) {
return true;
}
for (int i = n; i < m; ++i) { // 使用一个固定长度为n的滑动窗口来维护cnt2
// 滑动窗口每向右滑动一次(i++),就多统计一次进入窗口的字符,减少一次离开窗口的字符
++cnt2[s2[i] - 'a'];
--cnt2[s2[i - n] - 'a'];
if (equals(cnt1, cnt2)) { // 判断排列是否相等
return true;
}
}
return false;
}
443. 压缩字符串
手法1:不要使用 itoa 函数了,可以使用 sprintf 函数做int转字符.
我的代码: 可以通过,但内存消耗有点大:
void combine(char *str, int cnt, int *pos)
{
char *tmpStr = NULL;
int len = 0;
int cntTmp = cnt;
int i = 0;
while (cntTmp > 0) {
cntTmp = cntTmp / 10;
len++;
}
tmpStr = (char *)malloc(sizeof(char) * (len + 1));
sprintf_s(tmpStr, len + 1, "%d", cnt);
if (cnt == 1) {
return;
}
while (i < len) {
str[*pos] = tmpStr[i];
*pos = *pos + 1;
i++;
}
free(tmpStr);
return;
}
int compress(char *chars, int charsSize)
{
char strR;
char strL;
char *res = (char *)malloc(sizeof(char) * (charsSize + 1)); // 字符串的处理一定要多拷贝一个
int r = 0; // 统一都从0开始处理,方便容易推导
int l = 0;
int cnt = 0;
int pos = 0;
if (charsSize == 1) {
return 1;
}
while (r < charsSize) {
strL = chars[l];
strR = chars[r];
if (strR == strL) {
cnt++;
r++;
} else {
l = r;
res[pos] = strL;
pos++;
combine(res, cnt, &pos);
cnt = 0;
}
// 处理最后达到的情况
if (r == charsSize) {
res[pos] = strL;
pos++;
combine(res, cnt, &pos);
cnt = 0;
}
}
memcpy(chars, res, pos); // 这里没有拷贝 strlen(res) + 1的原因是,chars并没有这多么空间,用例 'a','a' -》 'a2'
free(res);
return pos;
}
int main() {
char chars[2] = {'a', 'a'};
int size = 2;
int res = compress((char *)chars, size);
return 0;
}
209. 长度最小的子数组
对于滑动窗口一定注意,算法复杂度,一般不会使用 O(n^2),要做些优化
// 朴素的滑动窗口 O(n^2)
int arraySum(int *nums, int left, int right)
{
int res = 0;
for (int i = left; i <= right; i++) {
res = res + nums[i];
}
return res;
}
int minSubArrayLen(int target, int *nums, int numsSize)
{
int left = 0;
int right = 0;
int res = 0;
int step = INT_MAX;
while (left <= right && right < numsSize) {
res = arraySum(nums, left, right);
if (res >= target) {
step = fmin(step, right - left + 1);
left++;
} else {
right++;
}
}
if (step == INT_MAX) {
step = 0;
}
return step;
}
// 法2:优化的滑动
int minSubArrayLen(int target, int* nums, int numsSize){
// 初始化最小长度为INT_MAX
int step = INT_MAX;
int res = 0;
int length = 0;
int left = 0, right;
// 右边界向右扩展
for(right = 0; right < numsSize; ++right) {
res += nums[right];
// 当sum的值大于等于target时,保存长度,并且收缩左边界
while(res >= target) {
length = right - left + 1;
step = fmin(step, length);
res -= nums[left++];
}
}
// 若minLength不为INT_MAX,则返回minLnegth
return step == INT_MAX ? 0 : step;
}
1004. 最大连续1的个数 III
int longestOnes(int *nums, int numsSize, int k)
{
int left = 0;
int right = 0;
int lsum = 0, rsum = 0;
int ans = 0;
int i = 0;
for (i = 0; i < numsSize; i++) {
nums[i] = fabs(nums[i] - 1);
}
// 1. 初始化区间
// 2. 保持区间 [left, right]
for (right = 0; right < numsSize; ++right)
{
rsum += nums[right]; // Right一直加,加多了再考虑移动Left做减法
// 3. 终止条件
while (rsum - lsum > k && right < numsSize) {
lsum += nums[left]; //符合终止条件时,Left开始移动
++left;
}
ans = fmax(ans, right - (left - 1));
}
return ans;
}