1. 数组
1.1. 二分查找
-
STL中关于二分查找的函数有三个
lower_bound、upper_bound、binary_search
。这三个函数都运用于有序区间.ForwardIter lower_bound(ForwardIter first, ForwardIter last, const _Tp &val)
算法返回一个非递减序列[first, last)
中的第一个大于等于值val的位置;ForwardIter upper_bound(ForwardIter first, ForwardIter last, const _Tp& val)
算法返回一个非递减序列[first, last)
中的第一个大于值val的位置;
-
相关题目:
- #704 二分查找
- #35 搜索插入位置
- #34 在排序数组中查找元素的第一个和最后一个位置
- #69 x x x 的平方根
- #367 有效的完全平方数
- #33 搜索旋转排序数组
- #81 搜索旋转排序数组 II
- #154 寻找旋转排序数组中的最小值 II(待完成)
- #540 有序数组中的单一元素(待完成)
- #4 寻找两个正序数组的中位数(待完成)
1.1.1. #704 二分查找
// 左闭右闭写法
int binSearch(vector<int> &nums, int target)
{
int l = 0, r = nums.size() - 1;
while (l <= r)
{
int mid = (l + r) / 2;
if (nums[mid] > target)
r = mid - 1;
else if (nums[mid] < target)
l = mid + 1;
else
return mid;
}
return -1;
}
// 左闭右开写法
int binSearch(vector<int> &nums, int target)
{
int l = 0, r = nums.size();
while (left < right)
{
int mid = (l + r) / 2;
if (nums[mid] > target)
r = mid;
else if (nums[mid] < target)
l = mid + 1;
else
return mid;
}
return -1;
}
1.1.2. #35 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。
// 暴力解法
int searchInsert(vector<int> &nums, int target)
{
for (int i = 0; i < nums.size(); i++)
{
// 处理如下三种情况:目标值在数组所有元素之前;等于数组中某一个元素;插入数组中的位置
if (nums[i] >= target)
return i;
}
}
// 目标值在数组所有元素之后的情况
return nums.size();
}
// 二分法
int searchInsert(vector<int> &nums, int target)
{
int n = nums.size();
// 插入位置为最左边
if (n < 1 || target < nums[0])
return 0;
// 插入位置在最右边
if (target > nums[n - 1])
return n;
// 目标值存在数组内
int l = 0, r = n - 1;
while (l <= r)
{
int mid = (l + r) / 2;
if (nums[mid] == target)
return mid;
else if (nums[mid] > target)
r = mid - 1;
else
l = mid + 1;
}
// 插入位置为数组内部
return r + 1;
}
1.1.3. #34 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组nums
,和一个目标值target
。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值target
,返回[-1, -1]
。
进阶:你可以设计并实现时间复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)的算法解决此问题吗?
// 暴力解法
vector<int> searchRange(vector<int> &nums, int target)
{
vector<int> ret = {-1, -1};
int n = nums.size();
if (n < 1)
return ret;
for (int i = 0; i < n; ++i)
{
if (nums[i] == target)
{
ret[0] = ret[1] = i;
int j = i + 1;
while (j < n && nums[j] == target)
++j;
ret[1] = j - 1;
break;
}
}
return ret;
}
// 二分查找
vector<int> searchRange(vector<int> &nums, int target)
{
if (nums.empty())
return vector<int>{-1, -1};
int lower = lower_bound(nums, target);
int upper = upper_bound(nums, target) - 1; // 这里需要减1位
if (lower == nums.size() || nums[lower] != target)
return vector<int>{-1, -1};
return vector<int>{lower, upper};
}
// 辅函数
int lower_bound(vector<int> &nums, int target)
{
int l = 0, r = nums.size() - 1, mid;
while (l <= r)
{
mid = (l + r) / 2;
if (nums[mid] >= target)
r = mid - 1;
else
l = mid + 1;
}
return l;
}
// 辅函数
int upper_bound(vector<int> &nums, int target)
{
int l = 0, r = nums.size() - 1, mid;
while (l <= r)
{
mid = (l + r) / 2;
if (nums[mid] > target)
r = mid - 1;
else
l = mid + 1;
}
return l;
}
1.1.4. #69 x 的平方根
实现int sqrt(int x)
函数。计算并返回x
的平方根,其中x
是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
// 二分查找(左闭右闭)思想
int mySqrt(int x)
{
if (x == 0)
return 0;
long long l = 1, r = x, mid = 0;
while (l <= r)
{
mid = (l + r) / 2;
if (mid * mid < x)
l = mid + 1;
else if (mid * mid > x)
r = mid - 1;
else
return mid;
}
return r;
}
// 牛顿迭代法???
1.1.5. #367 有效的完全平方数
bool isPerfectSquare(int num)
{
if (num == 0)
return true;
long long l = 1, r = num, mid = 0;
while (l <= r)
{
mid = (l + r) / 2;
if (mid * mid > num)
r = mid - 1;
else if (mid * mid < num)
l = mid + 1;
else
return true;
}
return false;
}
1.1.6. #33 搜索旋转排序数组
整数数组nums
按升序排列,数组中的值互不相同。在传递给函数之前,nums
在预先未知的某个下标k(0 <= k < nums.length)
上进行了旋转,使数组变为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标从0
开始计数)。给你旋转后的数组nums
和一个整数target
,如果nums
中存在这个目标值target
,则返回它的下标,否则返回-1
。
int search(vector<int> &nums, int target)
{
int n = nums.size();
int l = 0, r = n - 1, mid = 0;
while (l <= r)
{
mid = (l + r) / 2;
if (nums[mid] == target) return mid;
if (nums[mid] >= nums[l]) // 数组左边有序
{
if (nums[mid] > target && nums[l] <= target) // 目标值是否在左边有序数组内?
r = mid - 1;
else
l = mid + 1;
}
else if (nums[mid] <= nums[r]) // 数组右边有序
{
if (nums[mid] < target && nums[r] >= target) // 目标值是否在右边有序数组内?
l = mid + 1;
else
r = mid - 1;
}
}
return -1;
}
1.1.7. #81 搜索旋转排序数组 II
相对于#33,数组中的存在重复的值
// 时间复杂度:O(n),最坏情况下数组元素均相等且不为target,我们需要访问所有位置才能得出果。
// 空间复杂度:O(1)。
bool search(vector<int> &nums, int target)
{
int n = nums.size();
int l = 0, r = n - 1, mid = 0;
while (l <= r)
{
mid = (l + r) / 2;
if (nums[mid] == target)
return true;
if (nums[mid] < nums[r]) // 右区间有序
{
if (nums[mid] < target && nums[r] >= target)
l = mid + 1;
else
r = mid - 1;
}
else if (nums[mid] > nums[l]) // 左区间有序
{
if (nums[mid] > target && nums[l] <= target)
r = mid - 1;
else
l = mid + 1;
}
else if (nums[mid] == nums[l]) // 无法区分左区间是否有序
++l;
else if (nums[mid] == nums[r]) // 无法区分右区间是否有序
--r;
}
return false;
}
1.1.8. #154 寻找旋转排序数组中的最小值 II
1.1.9. #540 有序数组中的单一元素
1.1.10. #4 寻找两个正序数组的中位数
1.2. 双指针
- 双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
- 若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
- 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。
- 相关题目:
- #15 三数之和
- #18 四数之和
- #27 移除元素
- #26 删除排序数组中的重复项
- #283 移动零
- #844 比较含退格的字符串
- #977 有序数组的平方
- #167 两数之和 II - 输入有序数组
- #88 合并两个有序数组
- #142 环形链表 II
1.2.1. #15 三数之和
// 双指针法,注意去除重复元组
vector<vector<int>> threeSum(vector<int> &nums)
{
vector<vector<int>> ans;
if (nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size() - 2; ++i)
{
if (nums[i] > 0) return ans; // 排序后若首元素大于0,则不存在三数之和大于0
if (i > 0 && nums[i] == nums[i - 1]) continue; // 去除重复的元组
int left = i + 1, right = nums.size() - 1;
while (left < right)
{
if (nums[i] + nums[left] + nums[right] < 0) ++left;
else if (nums[i] + nums[left] + nums[right] > 0) --right;
else
{
ans.push_back({nums[i], nums[left], nums[right]});
while (left < right && nums[left + 1] == nums[left]) ++left;
while (left < right && nums[right - 1] == nums[right]) --right;
++left; --right;
}
}
}
return ans;
}
1.2.2. #18 四数之和
vector<vector<int>> fourSum(vector<int> &nums, int target)
{
vector<vector<int>> ans;
if (nums.size() < 4) return ans;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size() - 3; ++i)
{
if (i > 0 && nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < nums.size() - 2; ++j)
{
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
int left = j + 1, right = nums.size() - 1;
while (left < right)
{
if (nums[i] + nums[j] + nums[left] + nums[right] < target) ++left;
else if (nums[i] + nums[j] + nums[left] + nums[right] > target) --right;
else
{
ans.push_back({nums[i], nums[j], nums[left], nums[right]});
while (left < right && nums[left + 1] == nums[left]) ++left;
while (left < right && nums[right - 1] == nums[right]) --right;
++left; --right;
}
}
}
}
return ans;
}
1.2.3. #27 移除元素
// 快慢指针,通过fast指针找到下一个不等于val的元素放到slow位置
int removeElement(vector<int> &nums, int val)
{
int fast = 0, slow = 0;
while (fast < nums.size())
{
if (val == nums[fast])
fast++;
else
nums[slow++] = nums[fast++];
}
return slow;
}
1.2.4. #26 删除排序数组中的重复项
// 通过fast指针找到第一个不重复的元素放到slow位置
int removeDuplicates(vector<int> &nums)
{
if (nums.size() <= 1)
return nums.size();
int fast = 1, slow = 1;
while (fast < nums.size())
{
if (nums[fast] == nums[fast - 1])
fast++;
else
nums[slow++] = nums[fast++];
}
return slow;
}
1.2.5. #283 移动零
void moveZeroes(vector<int> &nums)
{
int fast = 0, slow = 0;
while (fast < nums.size())
{
if (nums[fast] == 0)
fast++;
else
nums[slow++] = nums[fast++];
}
for (int i = slow; i < nums.size(); ++i)
nums[i] = 0;
}
1.2.6. #844 比较含退格的字符串
// 从后往前比,某个字符是否被删除掉,只与其前面的'#'有关
// string S = "bxj##tw", T = "bxo#j##tw"
bool backspaceCompare(string S, string T)
{
int i = S.size() - 1, j = T.size() - 1;
while (i >= 0 || j >= 0)
{
int delSNum = 0, delTNum = 0;
// 找到S中第一个要比较的字符
while (i >= 0)
{
if (S[i] == '#')
{
++delSNum;
--i;
}
else if (delSNum > 0)
{
--delSNum;
--i;
}
else
break;
}
// 找到T中第一个要比较的字符
while (j >= 0)
{
if (T[j] == '#')
{
++delTNum;
--j;
}
else if (delTNum > 0)
{
--delTNum;
--j;
}
else
break;
}
if (i < 0 && j < 0)
return true;
if ((i < 0 && j >= 0) || (i >= 0 && j < 0))
return false;
if (S[i] != T[j])
return false;
else
{
--i;
--j;
}
}
return true;
}
1.2.7. #977 有序数组的平方
vector<int> sortedSquares(vector<int> &nums)
{
vector<int> ret(nums.size(), 0);
int pos = nums.size() - 1;
for (int i = 0, j = nums.size() - 1; i <= j;)
{
if (nums[i] * nums[i] < nums[j] * nums[j])
{
ret[pos--] = nums[j] * nums[j];
j--;
}
else
{
ret[pos--] = nums[i] * nums[i];
i++;
}
}
return ret;
}
1.2.8. #167 两数之和 II - 输入有序数组
vector<int> twoSum(vector<int> &numbers, int target)
{
vector<int> ret(2, 0);
int l = 0, r = numbers.size() - 1;
while (l < r)
{
if (numbers[l] + numbers[r] == target)
{
ret[0] = l + 1;
ret[1] = r + 1;
return ret;
}
if (numbers[l] + numbers[r] < target) // 两数和小于目标值,左指针右移一位
++l;
else // 两数和大于目标值,右指针左移一位
--r;
}
return ret;
}
1.2.9. #88 合并两个有序数组
void merge(vector<int> &nums1, int m, vector<int> &nums2, int n)
{
int p1 = m - 1, p2 = n - 1, p3 = m + n - 1;
while (p2 >= 0)
{
if (p1 < 0)
{
for (int i = 0; i <= p2; ++i)
nums1[i] = nums2[i];
break;
}
if (nums1[p1] < nums2[p2])
nums1[p3--] = nums2[p2--];
else
nums1[p3--] = nums1[p1--];
}
}
1.2.10. #142 环形链表 II
对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd判圈法)。给定两个指针,分别命名为 slow
和fast
,起始位置在链表的开头。每次fast
前进两步,slow
前进一步。如果fast
可以走到尽头,那么说明没有环路;如果fast
可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow
和fast
相遇。当slow
和fast
第一次相遇时,我们将fast
重新移动到链表开头,并让slow
和fast
每次都前进一步。当slow
和fast
第二次相遇时,相遇的节点即为环路的开始点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
ListNode *detectCycle(ListNode *head)
{
ListNode *fast = head, *slow = head;
do
{
if (!fast || !fast->next) // fast是否可以走到尽头?
return nullptr;
fast = fast->next->next; // fast走两步
slow = slow->next; // slow走一步
} while (fast != slow); // 当fast和slow首次相遇时代表存在环路
fast = head;
while (fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return fast;
}
1.3. 滑动窗口
- 相关题目
- #209 长度最小的子数组
- #904 水果成篮
- #76 最小覆盖子串
1.3.1. #209 长度最小的子数组
// 暴力解法,以数组中的每个元素为起点开始
int minSubArrayLen(int target, vector<int> &nums)
{
int minLen = nums.size() + 1;
for (int i = 0; i < nums.size(); ++i)
{
int sum = 0, j = i;
while (sum < target)
{
sum += nums[j++];
if (j >= nums.size())
break;
}
if (sum >= target && minLen > j - i)
minLen = j - i;
}
if (minLen != nums.size() + 1)
return minLen;
else
return 0;
}
// 滑动窗口解法
int minSubArrayLen(int target, vector<int> &nums)
{
if (nums.size() == 0)
return 0;
int start = 0, end = 0, minLen = nums.size() + 1;
int sum = 0;
while (end < nums.size())
{
sum += nums[end];
while (sum >= target)
{
minLen = min(minLen, end - start + 1);
sum -= nums[start];
++start;
}
++end;
}
return (minLen == nums.size() + 1) ? 0 : minLen;
}
1.3.2. #904 水果成篮
剥掉马甲该题本质就是求最多包含两个(k=2)不同字符的最大窗口大小。
int totalFruit(vector<int> &tree)
{
unordered_map<int, int> window;
const int k = 2;
int start = 0, end = 0, maxLen = 0;
while (end < tree.size())
{
window[tree[end]]++;
// 当窗口内包含了多于k个不同的字符时,滑动窗口左端点直至窗口内只包含两种不同的字符
while (window.size() > k)
{
window[tree[start]]--;
if (window[tree[start]] == 0)
window.erase(tree[start]);
++start;
}
maxLen = max(maxLen, end - start + 1);
++end;
}
return maxLen;
}
1.3.3. #76 最小覆盖子串
string minWindow(string S, string T)
{
// 先统计T中的字符情况,同时need中记录了窗口内需要的字符情况
unordered_map<char, int> need;
for (int i = 0; i < T.size(); ++i)
need[T[i]] = 0;
for (int i = 0; i < T.size(); ++i)
++need[T[i]];
int start = 0, end = 0, minStart = 0, minLen = S.size() + 1, needNum = T.size();
while (end < S.size())
{
// 窗口内增加了一个需要的字符
if (need.find(S[end]) != need.end())
{
// 将该字符所需数目减1,当该字符所需数目仍大于0时,将总的所需字符目减1
if (--need[S[end]] >= 0)
--needNum;
// 若目前滑动窗口已包含T中全部字符,则尝试将l右移,在不影响结果的情况下获得最短子字符串
while (needNum == 0)
{
if (minLen > end - start + 1) // 该窗口比原来满足需求的窗口小?
{
minLen = end - start + 1;
minStart = start;
}
// 窗口端点左移可能会使得所需字符数变大
if ((need.find(S[start]) != need.end()) && ++need[S[start]] > 0)
++needNum;
++start;
}
}
++end;
}
return (minLen == S.size() + 1) ? "" : S.substr(minStart, minLen);
}
2. 字符串
2.1. 引言
2.2. 相关题目
- #344 反转字符串
- #541 反转字符串II
- #151 翻转字符串里的单词
- #28 实现
strStr()
(KMP算法) - #459 重复的子字符串
2.2.1. #541 反转字符串II
string reverseStr(string s, int k)
{
for (int i = 0; i < s.size(); i += 2 * k)
{
if (s.size() - i >= k)
reverse(s, i, i + k - 1);
else
reverse(s, i, s.size() - 1);
}
return s;
}
void reverse(string &s, int left, int right)
{
while (left < right)
{
swap(s[left], s[right]);
++left;
--right;
}
}
2.2.2. #151 翻转字符串里的单词
string reverseWords(string s)
{
int n = s.size();
int l = n - 1, r = n - 1;
string ans;
while (l >= 0)
{
// 跳过空格找到字母
while (l >= 0 && s[l] == ' '){ --l; --r; }
// 记录一个单词的范围:[l+1, r]
while (l >= 0 && s[l] != ' ') --l;
if (l != r) //代表找到一个单词
{
for (int i = l + 1; i <= r; ++i)
ans.push_back(s[i]);
ans.push_back(' ');
r = l;
}
}
// 后面会多加一个空格
if (ans[ans.size() - 1] == ' ')
ans.pop_back();
return ans;
}
// 原址处理,空间复杂度为O(1)的方法
// 移除多余空格,将字符串整体反转,反转字符串内的每一个单词
string reverseWords(string s)
{
int l = 0, r = 0, end = s.size() - 1;
while (r < s.size() && s[r] == ' ') ++r; //去除起始的空格
while (end >= 0 && s[end] == ' ') --end; //去除末尾空格
while (r <= end) //使用快慢指针移除中间多余的空格
{
while (r <= end && s[r] != ' ') s[l++] = s[r++];
if (r < end)
{
s[l++] = s[r++]; //单词之间需要保留一个空格
while (r < s.size() && s[r] == ' ') ++r; //处理单词之间多余的空格
}
}
s.resize(l);
reverse(s.begin(), s.end()); //将字符串整体反转
l = 0, r = 0;
while (r < s.size())
{
while (r < s.size() && s[r] != ' ') ++r;
reverse(s.begin() + l, s.begin() + r); //反转字符串中的每一个的单词
++r; //跳过空格
l = r; //处理下一个单词
}
return s;
}
2.2.3. #28 实现strStr()
KMP算法思想:通过分析模式串的先验信息,使得当出现字符串不匹配时,可以只回退模式串,而不用回退源字符串。时间复杂度: O ( n + m ) O(n+m) O(n+m),至多需要遍历两字符串一次。
//对模式串进行先验分析
void getPi(vector<int> &pi, const string &s)
{
int j = 0;
pi[0] = 0;
for (int i = 1; i < s.size(); ++i)
{
while (j > 0 && s[i] != s[j]) j = pi[j - 1];
if (s[i] == s[j]) ++j;
pi[i] = j;
}
}
int strStr(string haystack, string needle)
{
int n = haystack.size(), m = needle.size();
if (m < 1) return 0;
if (m > n) return -1;
vector<int> pi(m, 0);
getPi(pi, needle);
int j = 0;
for (int i = 0; i < n; ++i)
{
while (j > 0 && haystack[i] != needle[j]) j = pi[j - 1];
if (haystack[i] == needle[j]) ++j;
if (j == m) return i - m + 1;
}
return -1;
}
2.2.4. #459 重复的子字符串
bool repeatedSubstringPattern(string s)
{
int n = s.size();
if (n == 0) return false;
vector<int> pi(n, 0);
pi[0] = 0;
int j = 0;
for (int i = 1; i < n; ++i)
{
while (j > 0 && s[i] != s[j]) j = pi[j - 1];
if (s[i] == s[j]) ++j;
pi[i] = j;
}
if (pi[n - 1] != 0 && n % (n - pi[n - 1]) == 0)
return true;
return false;
}
1. 数组
1.1. 二分查找
-
STL中关于二分查找的函数有三个
lower_bound、upper_bound、binary_search
。这三个函数都运用于有序区间.ForwardIter lower_bound(ForwardIter first, ForwardIter last, const _Tp &val)
算法返回一个非递减序列[first, last)
中的第一个大于等于值val的位置;ForwardIter upper_bound(ForwardIter first, ForwardIter last, const _Tp& val)
算法返回一个非递减序列[first, last)
中的第一个大于值val的位置;
-
相关题目:
- #704 二分查找
- #35 搜索插入位置
- #34 在排序数组中查找元素的第一个和最后一个位置
- #69 x x x 的平方根
- #367 有效的完全平方数
- #33 搜索旋转排序数组
- #81 搜索旋转排序数组 II
- #154 寻找旋转排序数组中的最小值 II(待完成)
- #540 有序数组中的单一元素(待完成)
- #4 寻找两个正序数组的中位数(待完成)
1.1.1. #704 二分查找
// 左闭右闭写法
int binSearch(vector<int> &nums, int target)
{
int l = 0, r = nums.size() - 1;
while (l <= r)
{
int mid = (l + r) / 2;
if (nums[mid] > target)
r = mid - 1;
else if (nums[mid] < target)
l = mid + 1;
else
return mid;
}
return -1;
}
// 左闭右开写法
int binSearch(vector<int> &nums, int target)
{
int l = 0, r = nums.size();
while (left < right)
{
int mid = (l + r) / 2;
if (nums[mid] > target)
r = mid;
else if (nums[mid] < target)
l = mid + 1;
else
return mid;
}
return -1;
}
1.1.2. #35 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。
// 暴力解法
int searchInsert(vector<int> &nums, int target)
{
for (int i = 0; i < nums.size(); i++)
{
// 处理如下三种情况:目标值在数组所有元素之前;等于数组中某一个元素;插入数组中的位置
if (nums[i] >= target)
return i;
}
}
// 目标值在数组所有元素之后的情况
return nums.size();
}
// 二分法
int searchInsert(vector<int> &nums, int target)
{
int n = nums.size();
// 插入位置为最左边
if (n < 1 || target < nums[0])
return 0;
// 插入位置在最右边
if (target > nums[n - 1])
return n;
// 目标值存在数组内
int l = 0, r = n - 1;
while (l <= r)
{
int mid = (l + r) / 2;
if (nums[mid] == target)
return mid;
else if (nums[mid] > target)
r = mid - 1;
else
l = mid + 1;
}
// 插入位置为数组内部
return r + 1;
}
1.1.3. #34 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组nums
,和一个目标值target
。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值target
,返回[-1, -1]
。
进阶:你可以设计并实现时间复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)的算法解决此问题吗?
// 暴力解法
vector<int> searchRange(vector<int> &nums, int target)
{
vector<int> ret = {-1, -1};
int n = nums.size();
if (n < 1)
return ret;
for (int i = 0; i < n; ++i)
{
if (nums[i] == target)
{
ret[0] = ret[1] = i;
int j = i + 1;
while (j < n && nums[j] == target)
++j;
ret[1] = j - 1;
break;
}
}
return ret;
}
// 二分查找
vector<int> searchRange(vector<int> &nums, int target)
{
if (nums.empty())
return vector<int>{-1, -1};
int lower = lower_bound(nums, target);
int upper = upper_bound(nums, target) - 1; // 这里需要减1位
if (lower == nums.size() || nums[lower] != target)
return vector<int>{-1, -1};
return vector<int>{lower, upper};
}
// 辅函数
int lower_bound(vector<int> &nums, int target)
{
int l = 0, r = nums.size() - 1, mid;
while (l <= r)
{
mid = (l + r) / 2;
if (nums[mid] >= target)
r = mid - 1;
else
l = mid + 1;
}
return l;
}
// 辅函数
int upper_bound(vector<int> &nums, int target)
{
int l = 0, r = nums.size() - 1, mid;
while (l <= r)
{
mid = (l + r) / 2;
if (nums[mid] > target)
r = mid - 1;
else
l = mid + 1;
}
return l;
}
1.1.4. #69 x 的平方根
实现int sqrt(int x)
函数。计算并返回x
的平方根,其中x
是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
// 二分查找(左闭右闭)思想
int mySqrt(int x)
{
if (x == 0)
return 0;
long long l = 1, r = x, mid = 0;
while (l <= r)
{
mid = (l + r) / 2;
if (mid * mid < x)
l = mid + 1;
else if (mid * mid > x)
r = mid - 1;
else
return mid;
}
return r;
}
// 牛顿迭代法???
1.1.5. #367 有效的完全平方数
bool isPerfectSquare(int num)
{
if (num == 0)
return true;
long long l = 1, r = num, mid = 0;
while (l <= r)
{
mid = (l + r) / 2;
if (mid * mid > num)
r = mid - 1;
else if (mid * mid < num)
l = mid + 1;
else
return true;
}
return false;
}
1.1.6. #33 搜索旋转排序数组
整数数组nums
按升序排列,数组中的值互不相同。在传递给函数之前,nums
在预先未知的某个下标k(0 <= k < nums.length)
上进行了旋转,使数组变为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标从0
开始计数)。给你旋转后的数组nums
和一个整数target
,如果nums
中存在这个目标值target
,则返回它的下标,否则返回-1
。
int search(vector<int> &nums, int target)
{
int n = nums.size();
int l = 0, r = n - 1, mid = 0;
while (l <= r)
{
mid = (l + r) / 2;
if (nums[mid] == target) return mid;
if (nums[mid] >= nums[l]) // 数组左边有序
{
if (nums[mid] > target && nums[l] <= target) // 目标值是否在左边有序数组内?
r = mid - 1;
else
l = mid + 1;
}
else if (nums[mid] <= nums[r]) // 数组右边有序
{
if (nums[mid] < target && nums[r] >= target) // 目标值是否在右边有序数组内?
l = mid + 1;
else
r = mid - 1;
}
}
return -1;
}
1.1.7. #81 搜索旋转排序数组 II
相对于#33,数组中的存在重复的值
// 时间复杂度:O(n),最坏情况下数组元素均相等且不为target,我们需要访问所有位置才能得出果。
// 空间复杂度:O(1)。
bool search(vector<int> &nums, int target)
{
int n = nums.size();
int l = 0, r = n - 1, mid = 0;
while (l <= r)
{
mid = (l + r) / 2;
if (nums[mid] == target)
return true;
if (nums[mid] < nums[r]) // 右区间有序
{
if (nums[mid] < target && nums[r] >= target)
l = mid + 1;
else
r = mid - 1;
}
else if (nums[mid] > nums[l]) // 左区间有序
{
if (nums[mid] > target && nums[l] <= target)
r = mid - 1;
else
l = mid + 1;
}
else if (nums[mid] == nums[l]) // 无法区分左区间是否有序
++l;
else if (nums[mid] == nums[r]) // 无法区分右区间是否有序
--r;
}
return false;
}
1.1.8. #154 寻找旋转排序数组中的最小值 II
1.1.9. #540 有序数组中的单一元素
1.1.10. #4 寻找两个正序数组的中位数
1.2. 双指针
- 双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
- 若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
- 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。
- 相关题目:
- #15 三数之和
- #18 四数之和
- #27 移除元素
- #26 删除排序数组中的重复项
- #283 移动零
- #844 比较含退格的字符串
- #977 有序数组的平方
- #167 两数之和 II - 输入有序数组
- #88 合并两个有序数组
- #142 环形链表 II
1.2.1. #15 三数之和
// 双指针法,注意去除重复元组
vector<vector<int>> threeSum(vector<int> &nums)
{
vector<vector<int>> ans;
if (nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size() - 2; ++i)
{
if (nums[i] > 0) return ans; // 排序后若首元素大于0,则不存在三数之和大于0
if (i > 0 && nums[i] == nums[i - 1]) continue; // 去除重复的元组
int left = i + 1, right = nums.size() - 1;
while (left < right)
{
if (nums[i] + nums[left] + nums[right] < 0) ++left;
else if (nums[i] + nums[left] + nums[right] > 0) --right;
else
{
ans.push_back({nums[i], nums[left], nums[right]});
while (left < right && nums[left + 1] == nums[left]) ++left;
while (left < right && nums[right - 1] == nums[right]) --right;
++left; --right;
}
}
}
return ans;
}
1.2.2. #18 四数之和
vector<vector<int>> fourSum(vector<int> &nums, int target)
{
vector<vector<int>> ans;
if (nums.size() < 4) return ans;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size() - 3; ++i)
{
if (i > 0 && nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < nums.size() - 2; ++j)
{
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
int left = j + 1, right = nums.size() - 1;
while (left < right)
{
if (nums[i] + nums[j] + nums[left] + nums[right] < target) ++left;
else if (nums[i] + nums[j] + nums[left] + nums[right] > target) --right;
else
{
ans.push_back({nums[i], nums[j], nums[left], nums[right]});
while (left < right && nums[left + 1] == nums[left]) ++left;
while (left < right && nums[right - 1] == nums[right]) --right;
++left; --right;
}
}
}
}
return ans;
}
1.2.3. #27 移除元素
// 快慢指针,通过fast指针找到下一个不等于val的元素放到slow位置
int removeElement(vector<int> &nums, int val)
{
int fast = 0, slow = 0;
while (fast < nums.size())
{
if (val == nums[fast])
fast++;
else
nums[slow++] = nums[fast++];
}
return slow;
}
1.2.4. #26 删除排序数组中的重复项
// 通过fast指针找到第一个不重复的元素放到slow位置
int removeDuplicates(vector<int> &nums)
{
if (nums.size() <= 1)
return nums.size();
int fast = 1, slow = 1;
while (fast < nums.size())
{
if (nums[fast] == nums[fast - 1])
fast++;
else
nums[slow++] = nums[fast++];
}
return slow;
}
1.2.5. #283 移动零
void moveZeroes(vector<int> &nums)
{
int fast = 0, slow = 0;
while (fast < nums.size())
{
if (nums[fast] == 0)
fast++;
else
nums[slow++] = nums[fast++];
}
for (int i = slow; i < nums.size(); ++i)
nums[i] = 0;
}
1.2.6. #844 比较含退格的字符串
// 从后往前比,某个字符是否被删除掉,只与其前面的'#'有关
// string S = "bxj##tw", T = "bxo#j##tw"
bool backspaceCompare(string S, string T)
{
int i = S.size() - 1, j = T.size() - 1;
while (i >= 0 || j >= 0)
{
int delSNum = 0, delTNum = 0;
// 找到S中第一个要比较的字符
while (i >= 0)
{
if (S[i] == '#')
{
++delSNum;
--i;
}
else if (delSNum > 0)
{
--delSNum;
--i;
}
else
break;
}
// 找到T中第一个要比较的字符
while (j >= 0)
{
if (T[j] == '#')
{
++delTNum;
--j;
}
else if (delTNum > 0)
{
--delTNum;
--j;
}
else
break;
}
if (i < 0 && j < 0)
return true;
if ((i < 0 && j >= 0) || (i >= 0 && j < 0))
return false;
if (S[i] != T[j])
return false;
else
{
--i;
--j;
}
}
return true;
}
1.2.7. #977 有序数组的平方
vector<int> sortedSquares(vector<int> &nums)
{
vector<int> ret(nums.size(), 0);
int pos = nums.size() - 1;
for (int i = 0, j = nums.size() - 1; i <= j;)
{
if (nums[i] * nums[i] < nums[j] * nums[j])
{
ret[pos--] = nums[j] * nums[j];
j--;
}
else
{
ret[pos--] = nums[i] * nums[i];
i++;
}
}
return ret;
}
1.2.8. #167 两数之和 II - 输入有序数组
vector<int> twoSum(vector<int> &numbers, int target)
{
vector<int> ret(2, 0);
int l = 0, r = numbers.size() - 1;
while (l < r)
{
if (numbers[l] + numbers[r] == target)
{
ret[0] = l + 1;
ret[1] = r + 1;
return ret;
}
if (numbers[l] + numbers[r] < target) // 两数和小于目标值,左指针右移一位
++l;
else // 两数和大于目标值,右指针左移一位
--r;
}
return ret;
}
1.2.9. #88 合并两个有序数组
void merge(vector<int> &nums1, int m, vector<int> &nums2, int n)
{
int p1 = m - 1, p2 = n - 1, p3 = m + n - 1;
while (p2 >= 0)
{
if (p1 < 0)
{
for (int i = 0; i <= p2; ++i)
nums1[i] = nums2[i];
break;
}
if (nums1[p1] < nums2[p2])
nums1[p3--] = nums2[p2--];
else
nums1[p3--] = nums1[p1--];
}
}
1.2.10. #142 环形链表 II
对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd判圈法)。给定两个指针,分别命名为 slow
和fast
,起始位置在链表的开头。每次fast
前进两步,slow
前进一步。如果fast
可以走到尽头,那么说明没有环路;如果fast
可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow
和fast
相遇。当slow
和fast
第一次相遇时,我们将fast
重新移动到链表开头,并让slow
和fast
每次都前进一步。当slow
和fast
第二次相遇时,相遇的节点即为环路的开始点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
ListNode *detectCycle(ListNode *head)
{
ListNode *fast = head, *slow = head;
do
{
if (!fast || !fast->next) // fast是否可以走到尽头?
return nullptr;
fast = fast->next->next; // fast走两步
slow = slow->next; // slow走一步
} while (fast != slow); // 当fast和slow首次相遇时代表存在环路
fast = head;
while (fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return fast;
}
1.3. 滑动窗口
- 相关题目
- #209 长度最小的子数组
- #904 水果成篮
- #76 最小覆盖子串
1.3.1. #209 长度最小的子数组
// 暴力解法,以数组中的每个元素为起点开始
int minSubArrayLen(int target, vector<int> &nums)
{
int minLen = nums.size() + 1;
for (int i = 0; i < nums.size(); ++i)
{
int sum = 0, j = i;
while (sum < target)
{
sum += nums[j++];
if (j >= nums.size())
break;
}
if (sum >= target && minLen > j - i)
minLen = j - i;
}
if (minLen != nums.size() + 1)
return minLen;
else
return 0;
}
// 滑动窗口解法
int minSubArrayLen(int target, vector<int> &nums)
{
if (nums.size() == 0)
return 0;
int start = 0, end = 0, minLen = nums.size() + 1;
int sum = 0;
while (end < nums.size())
{
sum += nums[end];
while (sum >= target)
{
minLen = min(minLen, end - start + 1);
sum -= nums[start];
++start;
}
++end;
}
return (minLen == nums.size() + 1) ? 0 : minLen;
}
1.3.2. #904 水果成篮
剥掉马甲该题本质就是求最多包含两个(k=2)不同字符的最大窗口大小。
int totalFruit(vector<int> &tree)
{
unordered_map<int, int> window;
const int k = 2;
int start = 0, end = 0, maxLen = 0;
while (end < tree.size())
{
window[tree[end]]++;
// 当窗口内包含了多于k个不同的字符时,滑动窗口左端点直至窗口内只包含两种不同的字符
while (window.size() > k)
{
window[tree[start]]--;
if (window[tree[start]] == 0)
window.erase(tree[start]);
++start;
}
maxLen = max(maxLen, end - start + 1);
++end;
}
return maxLen;
}
1.3.3. #76 最小覆盖子串
string minWindow(string S, string T)
{
// 先统计T中的字符情况,同时need中记录了窗口内需要的字符情况
unordered_map<char, int> need;
for (int i = 0; i < T.size(); ++i)
need[T[i]] = 0;
for (int i = 0; i < T.size(); ++i)
++need[T[i]];
int start = 0, end = 0, minStart = 0, minLen = S.size() + 1, needNum = T.size();
while (end < S.size())
{
// 窗口内增加了一个需要的字符
if (need.find(S[end]) != need.end())
{
// 将该字符所需数目减1,当该字符所需数目仍大于0时,将总的所需字符目减1
if (--need[S[end]] >= 0)
--needNum;
// 若目前滑动窗口已包含T中全部字符,则尝试将l右移,在不影响结果的情况下获得最短子字符串
while (needNum == 0)
{
if (minLen > end - start + 1) // 该窗口比原来满足需求的窗口小?
{
minLen = end - start + 1;
minStart = start;
}
// 窗口端点左移可能会使得所需字符数变大
if ((need.find(S[start]) != need.end()) && ++need[S[start]] > 0)
++needNum;
++start;
}
}
++end;
}
return (minLen == S.size() + 1) ? "" : S.substr(minStart, minLen);
}
2. 字符串
2.1. 引言
2.2. 相关题目
- #344 反转字符串
- #541 反转字符串II
- #151 翻转字符串里的单词
- #28 实现
strStr()
(KMP算法) - #459 重复的子字符串
2.2.1. #541 反转字符串II
string reverseStr(string s, int k)
{
for (int i = 0; i < s.size(); i += 2 * k)
{
if (s.size() - i >= k)
reverse(s, i, i + k - 1);
else
reverse(s, i, s.size() - 1);
}
return s;
}
void reverse(string &s, int left, int right)
{
while (left < right)
{
swap(s[left], s[right]);
++left;
--right;
}
}
2.2.2. #151 翻转字符串里的单词
string reverseWords(string s)
{
int n = s.size();
int l = n - 1, r = n - 1;
string ans;
while (l >= 0)
{
// 跳过空格找到字母
while (l >= 0 && s[l] == ' '){ --l; --r; }
// 记录一个单词的范围:[l+1, r]
while (l >= 0 && s[l] != ' ') --l;
if (l != r) //代表找到一个单词
{
for (int i = l + 1; i <= r; ++i)
ans.push_back(s[i]);
ans.push_back(' ');
r = l;
}
}
// 后面会多加一个空格
if (ans[ans.size() - 1] == ' ')
ans.pop_back();
return ans;
}
// 原址处理,空间复杂度为O(1)的方法
// 移除多余空格,将字符串整体反转,反转字符串内的每一个单词
string reverseWords(string s)
{
int l = 0, r = 0, end = s.size() - 1;
while (r < s.size() && s[r] == ' ') ++r; //去除起始的空格
while (end >= 0 && s[end] == ' ') --end; //去除末尾空格
while (r <= end) //使用快慢指针移除中间多余的空格
{
while (r <= end && s[r] != ' ') s[l++] = s[r++];
if (r < end)
{
s[l++] = s[r++]; //单词之间需要保留一个空格
while (r < s.size() && s[r] == ' ') ++r; //处理单词之间多余的空格
}
}
s.resize(l);
reverse(s.begin(), s.end()); //将字符串整体反转
l = 0, r = 0;
while (r < s.size())
{
while (r < s.size() && s[r] != ' ') ++r;
reverse(s.begin() + l, s.begin() + r); //反转字符串中的每一个的单词
++r; //跳过空格
l = r; //处理下一个单词
}
return s;
}
2.2.3. #28 实现strStr()
KMP算法思想:通过分析模式串的先验信息,使得当出现字符串不匹配时,可以只回退模式串,而不用回退源字符串。时间复杂度: O ( n + m ) O(n+m) O(n+m),至多需要遍历两字符串一次。
//对模式串进行先验分析
void getPi(vector<int> &pi, const string &s)
{
int j = 0;
pi[0] = 0;
for (int i = 1; i < s.size(); ++i)
{
while (j > 0 && s[i] != s[j]) j = pi[j - 1];
if (s[i] == s[j]) ++j;
pi[i] = j;
}
}
int strStr(string haystack, string needle)
{
int n = haystack.size(), m = needle.size();
if (m < 1) return 0;
if (m > n) return -1;
vector<int> pi(m, 0);
getPi(pi, needle);
int j = 0;
for (int i = 0; i < n; ++i)
{
while (j > 0 && haystack[i] != needle[j]) j = pi[j - 1];
if (haystack[i] == needle[j]) ++j;
if (j == m) return i - m + 1;
}
return -1;
}
2.2.4. #459 重复的子字符串
bool repeatedSubstringPattern(string s)
{
int n = s.size();
if (n == 0) return false;
vector<int> pi(n, 0);
pi[0] = 0;
int j = 0;
for (int i = 1; i < n; ++i)
{
while (j > 0 && s[i] != s[j]) j = pi[j - 1];
if (s[i] == s[j]) ++j;
pi[i] = j;
}
if (pi[n - 1] != 0 && n % (n - pi[n - 1]) == 0)
return true;
return false;
}