力扣排序五十五题——最优思路精选集(持续更新中)(C++)

目录

  • 242.有效的字母异位词
  • 56. 合并区间(双指针)
  • 215.数组中的第K个最大元素
  • 75 颜色分类(又称荷兰国旗问题)

242.有效的字母异位词

力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第1张图片
[博主完成两种实现用时: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;
    }
};

56. 合并区间(双指针)

题目描述:
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第2张图片
解题思路:排序 + 双指针
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第3张图片
代码实现:

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;
    }
};

215.数组中的第K个最大元素

力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第4张图片
这道题据说是面试常考题,那么考官既然摆了这样一道题,你直接用快排,用nth_element解决就有点过意不去了,我首推高级算法,花里胡哨乱秀那种,码完还能给面试官讲讲思路,好感度蹭蹭地上升。

思路一:最小堆
没错,我们曾经在堆排序里曾经讲过的最小堆、最大堆的常见应用:在m个值中选出最大的k个值,在这儿用上了。
还记得我当初参加速游的笔试时,最后一题的描述如下:请在m个数中找出最大的k个数(m非常大)。奈何我当时没学过堆排序,更不懂最大堆和最小堆,压根就没有思路,当时还想着用选择排序选择前k个数,时间复杂度为O(mk)。其实如果使用了最小堆/最大堆,时间复杂度能直接降至O(mlogk)。
接下来给大家分析一下利用最小堆如何解决这类型问题(在m个数中寻找最大的k个值/寻找第k大的值):

图解过程如下:
1.待排序数列作为参数传入,初始化最小堆(空数组)
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第5张图片
2.假设我们要寻找第 2 大的元素,将待排序数列中的前两个数压入堆中,并适当操作摆弄成最小堆的形式。
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第6张图片
3.将待排序数列中的下一个元素拿来与堆顶元素进行比较,比如这里我们用 1 和 堆顶的 2 进行比较,发现 1 比 2 小,说明大于 1 的元素至少有两个,1 不可能是第二大的元素,所以跳过该元素直接遍历下一个元素。
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第7张图片
4.继续拿入元素与栈顶元素进行比较,这里 5 比 2大,我们让栈顶元素等于 5 ,然后进行下沉操作。
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第8张图片
5.最后的结果如下,堆中元素即为原数组中最大的两个值,其中堆顶元素 5 是第二大的元素:
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第9张图片
这里有必要跟大家解释一下,实际待排序数列中的元素并没有被弹出去,图中不显示只是说明我们不考虑该元素而已。

代码实现如下:

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:
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第10张图片
使用库函数nth_element的算法复杂度
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第11张图片

75 颜色分类(又称荷兰国旗问题)

题目描述:
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第12张图片
思路一:计数排序
这里明确指出不能使用代码库中的排序函数来解决这道题,但是同样的,我们可以自己手撸代码呀。这里博主使用的是计数排序,执行用时和内存消耗还是很客观的。

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的最左边界和当前考虑的元素。
力扣排序五十五题——最优思路精选集(持续更新中)(C++)_第13张图片
本解法的思路是沿着数组移动 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)

你可能感兴趣的:(力扣刷题之路)