[博主完成两种实现用时:7分钟]
实现一: 思路:直接用sort算法。
class Solution {
public:
bool isAnagram(string s, string t) {
sort(s.begin(),s.end());
sort(t.begin(),t.end());
if(s==t)
return true;
else
return false;
}
};
实现二: 思路:使用哈希表存储字符与出现次数之间的映射,若s中出现则相应字符的映射(一个int类型的变量)+1,若t中出现则相应字符的映射 -1。
class Solution {
public:
bool isAnagram(string s, string t) {
unordered_map map;
if (s.size() != t.size())
return false;
for(int i=0;i::iterator it=map.begin();it!=map.end();it++)
if(it->second!=0)
return false;
return true;
}
};
实现三: 思路:巧妙地利用了题目只包含26个小写字母的假设,建立了一个长度为26的数组模拟hash映射。
class Solution {
public:
bool isAnagram(string s, string t) {
vector num(26);
if(s.size()!=t.size())
return false;
for(int i=0;s[i]!='\0';i++){
num[s[i]-'a']++;
num[t[i]-'a']--;
}
for(int i=0;i<26;i++)
if(num[i]!=0)
return false;
return true;
}
};
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if(intervals.size() <= 1)
return intervals;
//首先对原数组进行排序(vector默认逐元素比较排序)
sort(intervals.begin(),intervals.end());
//save指针用于保存最终的合并区间,scan用来遍历intervals
vector<vector<int>>::iterator save = intervals.begin();
vector<vector<int>>::iterator scan = save + 1;
vector<vector<int>> ans; //存放所有合并区间的数组
while(scan != intervals.end())
{
/*比较三种情况*/
/*一、save所指向的数组和scan所指向的数组为不重叠的关系 */
if((*scan)[0] > (*save)[1])
{
ans.push_back(*save); //直接将save指向的数组放入ans数组
save = scan; //save 指向 scan指向的数组
++scan; //scan指向下一个元素
}
/*二、save指向的数组包含scan所指向的数组 */
else if((*scan)[1] <= (*save)[1])
++scan;
/*三、save指向的数组与scan所指向的数组重叠 */
else
{
(*save)[1] = (*scan)[1];
++scan;
}
}
ans.push_back(*save); //当scan指向intervals尾部时,将save所指向的数组放入ans.
return ans;
}
};
这道题据说是面试常考题,那么考官既然摆了这样一道题,你直接用快排,用nth_element解决就有点过意不去了,我首推高级算法,花里胡哨乱秀那种,码完还能给面试官讲讲思路,好感度蹭蹭地上升。
思路一:最小堆
没错,我们曾经在堆排序里曾经讲过的最小堆、最大堆的常见应用:在m个值中选出最大的k个值,在这儿用上了。
还记得我当初参加速游的笔试时,最后一题的描述如下:请在m个数中找出最大的k个数(m非常大)。奈何我当时没学过堆排序,更不懂最大堆和最小堆,压根就没有思路,当时还想着用选择排序选择前k个数,时间复杂度为O(mk)。其实如果使用了最小堆/最大堆,时间复杂度能直接降至O(mlogk)。
接下来给大家分析一下利用最小堆如何解决这类型问题(在m个数中寻找最大的k个值/寻找第k大的值):
图解过程如下:
1.待排序数列作为参数传入,初始化最小堆(空数组)
2.假设我们要寻找第 2 大的元素,将待排序数列中的前两个数压入堆中,并适当操作摆弄成最小堆的形式。
3.将待排序数列中的下一个元素拿来与堆顶元素进行比较,比如这里我们用 1 和 堆顶的 2 进行比较,发现 1 比 2 小,说明大于 1 的元素至少有两个,1 不可能是第二大的元素,所以跳过该元素直接遍历下一个元素。
4.继续拿入元素与栈顶元素进行比较,这里 5 比 2大,我们让栈顶元素等于 5 ,然后进行下沉操作。
5.最后的结果如下,堆中元素即为原数组中最大的两个值,其中堆顶元素 5 是第二大的元素:
这里有必要跟大家解释一下,实际待排序数列中的元素并没有被弹出去,图中不显示只是说明我们不考虑该元素而已。
代码实现如下:
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
vector<int> help;
help.reserve(k);
//构建堆
for(size_t i = 0; i < k; ++i)
{
help.push_back(nums[i]);
//上浮
int current_index = help.size() - 1;
int parent_index = (current_index - 1) / 2;
while(parent_index >= 0 && help[current_index] < help[parent_index])
{
swap(help[current_index], help[parent_index]);
current_index = parent_index;
parent_index = (current_index - 1) / 2;
}
}
/* 现在我们构建了大小为 k 的最小堆,我们的目的是找出数组第 k 大的值
反过来,我们可以通过最小堆排除 n - k 个比该值小的元素,使得最后
最小堆中堆顶的元素为原数组中第k大的值
*/
for(int i = k; i < nums.size(); ++i)
{
//如果待插入元素比最小堆根节点的值要大,说明根节点的值已经不可能是第 k 大的值,比它大的值至少有 k 个
if(help.front() < nums[i])
{
help.front() = nums[i];
//下沉
int parent_index = 0,child_index = parent_index * 2 + 1;
while(child_index < k)
{
if(child_index + 1 < k && help[child_index] > help[child_index + 1])
child_index += 1;
if(help[parent_index] > help[child_index])
{
swap(help[parent_index], help[child_index]);
parent_index = child_index;
child_index = parent_index * 2 + 1;
}
else
break;
}
}
}
return help.front();
}
};
算法复杂度:
时间复杂度 : O ( N log k ) O(N \log k) O(Nlogk)。
空间复杂度 : O ( k ) O(k) O(k),用于存储堆元素。
思路二:不能用nth_element库函数我们还不能自己实现吗?题目让我们选出第k大的元素,实际就是让我们选出第(数组长度 - k + 1)小的元素,自己审审是不是这个理。
而实际上经典的算法思想里面,有一种名为快速选择的算法,恰恰实现的就是类似nth_element的功能:从m个数中选择第 k + 1 小的数,并将其放置在下标为 k 的位置。快速选择类似于快速排序中的partition部分,它只负责选择分界值并比较分界值是否为我们所需的目标值,而不会对分界值两边的元素进行排序。
由于我们已经讲解过快速排序的思想了,所以这里就不多加阐述。唯一值得提的点是,这里的分界值选择的是[left, right]范围内的随机值,这里就解决了算法在面对高度有序的数组时效率反而更低的缺陷。
/**利用快速排序中的partition思想解决,时间复杂度O(N); 空间复杂度O(1) **/
class Solution {
public:
int findKthLargest(vector<int>& nums, int k)
{
srand(time(NULL));
return quick_select(nums, 0, nums.size() - 1, nums.size() - k + 1);
}
private:
int quick_select(vector<int>& nums, int left, int right, int k)
{
int index = partition(nums, left, right);
if(index + 1 == k) return nums[index];
if(index + 1 < k) return quick_select(nums, index + 1, right, k);
if(index + 1 > k) return quick_select(nums, left, index - 1, k);
return - 1;
}
int partition(vector<int>& nums, int left, int right)
{
int rand_index = rand() % (right - left + 1) + left;
swap(nums[left],nums[rand_index]);
int temp = nums[left];
while(left < right)
{
while(nums[right] > temp)
--right;
nums[left] = nums[right];
while(left < right && nums[left] <= temp)
++left;
nums[right] = nums[left];
}
nums[left] = temp;
return left;
}
};
复杂度分析
时间复杂度 : 平均情况 O ( N ) O(N) O(N),最坏情况 O ( N 2 ) O(N ^2) O(N2)。
空间复杂度 : O ( 1 ) O(1) O(1)。
最后执行时间能堪比库函数nth_element:
使用库函数nth_element的算法复杂度
题目描述:
思路一:计数排序
这里明确指出不能使用代码库中的排序函数来解决这道题,但是同样的,我们可以自己手撸代码呀。这里博主使用的是计数排序,执行用时和内存消耗还是很客观的。
class Solution {
public:
void sortColors(vector<int>& nums) {
vector<int> help(3);
for(size_t i = 0; i < nums.size(); ++i)
++help[nums[i] - 0];
int index = 0;
for(size_t i = 0; i < 3; ++i)
while(help[i] > 0)
{
nums[index++] = i;
--help[i];
}
}
};
算法复杂度:
时间复杂度: O ( N ) O(N) O(N)
空间复杂度: O ( k ) O(k) O(k) 本题中 k = 3, 为help数组的元素个数。
思路二:三路快排
我们直接来看力扣官方提供的题解:
我们用三个指针(low, high 和i)来分别追踪0的最右边界,2的最左边界和当前考虑的元素。
本解法的思路是沿着数组移动 i 指针,若nums[i] = 0,则将其与 nums[low]互换;若 nums[i] = 2 ,则与 nums[high]互换。
算法
初始化0的最右边界:low = 0。在整个算法执行过程中 nums[idx < low] = 0.
初始化2的最左边界 :high = n - 1。在整个算法执行过程中 nums[idx > high] = 2.
初始化当前考虑的元素序号 :i = 0.
While i <= high :
若 nums[i] = 0 :交换 nums[i] 和 nums[low],并将指针都向右移。
若 nums[i] = 2 :交换nums[i] 和 nums[high],并将 high指针左移 。
若 nums[i] = 1 :将指针i右移。
代码实现:
class Solution {
public:
void sortColors(vector<int>& nums) {
//三路快排
int low = 0, high = nums.size() - 1;
int i = 0;
while(i <= high){
if(nums[i] == 0){
swap(nums[i],nums[low]);
++low; ++i;
}else if(nums[i] == 1){
++i;
}else if(nums[i] == 2){
swap(nums[i],nums[high]);
--high;
}
}
}
};
复杂度分析
时间复杂度 :由于对长度 N的数组进行了一次遍历,时间复杂度为 O ( N ) O(N) O(N) 。
空间复杂度 :由于只使用了常数空间,空间复杂度为 O ( 1 ) O(1) O(1) 。