【c++】力扣算法刷题 + 算法常用思想

文章目录

  • 一、题目
    • 1.两数之和(考点:哈希表)
    • 2.两数相加***(考点:单链表)
    • 3.无重复字符考点:(滑动窗口)
    • 4.寻找两个正序数组的中位数***(考点:二分法)O(log (m+n))
    • 5.最长回文子串(考点:动态规划、中心扩散)
    • 6.N 字形变换
    • 7. 整数反转
    • 8. 字符串转换整数 (atoi)
    • 9. 回文数
    • 10. 正则表达式匹配
    • 11.盛最多水的容器
    • 12. 整数转罗马数字
    • 14. 最长公共前缀
    • 15. 三数之和:考点双指针
    • 16. 最接近的三数之和:考点双指针,abs
    • 17. 电话号码的字母组合:考点回溯思想(想象成二叉树回溯思考)
    • 18.四数之和
  • 二、算法常用基本思想
    • 1.双指针
    • 2.分治:数组的归并排序
      • 例1:**合并两个--->合并区间---->list合并**
      • 例2:单链表排序
      • 例3:数组中的逆序对
    • 3.堆栈队列
      • (1)大顶堆、小顶堆
        • **例1** 最小的K个数(需要vector进行存储)
        • 例2 第K个大的数(其实也可以用priority_queue ,greater< int >)
    • 4.贪心算法
    • 5.回溯

一、题目

1.两数之和(考点:哈希表)

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

法1:时间复杂度O(n^2),空间复杂度O(1)

没看清题目要求,返回的应该是下标!

//执行用时:
//内存消耗:9.9 MB, 在所有 C++ 提交中击败了71.59%的用户
//法1:时间复杂度O(n^2),空间复杂度O(1)
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        /*vector vright;
        扩展:返回里面的数
        for(vector::iterator it=nums.begin();it!=nums.end();it++)
        {
            for(vector::iterator vit=++nums.begin();vit!=nums.end();vit++)
            {
                if(*it+*vit==target)
                {
                    vright.push_back(*it);
                    vright.push_back(*vit);
                    break;
                };
            }
        }
        */
        int num=nums.size();
        vector<int> vright;
        for(int i=0;i<num;i++)
        {
            for(int j=i+1;j<num;j++)
            {
                if(nums[i]+nums[j]==target)
                {
                    vright.push_back(i);
                    vright.push_back(j);
                    break;
                };
            }
        }
        //cout<<"["<< n1 <<","<< n2 <<"]"<
        return vright;//{i,j}
    }
};

【c++】力扣算法刷题 + 算法常用思想_第1张图片
法2:时间复杂度O(n),空间复杂度O(n)变大了

    vector<int> twoSum(vector<int>& nums, int target) {
        //法2:哈希表(内部无序)
        //因为要用下标,因此使用map进行存储???????这个数是怎么进去的?
        unordered_map<int, int> hashtable;
        for (int i = 0; i < nums.size(); ++i) {
            auto it = hashtable.find(target - nums[i]);//find只能找key,因此将数放在key中
            if (it != hashtable.end()) {//找到这个数
                return {it->second, i};
            }
            hashtable[nums[i]] = i;//没有找到该数,则将数放入,在这里将数据放入的
        }
        return {};
    }

两遍哈希表,我的评价是不如法2
【c++】力扣算法刷题 + 算法常用思想_第2张图片

2.两数相加***(考点:单链表)

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

  • 看定义,想象成数据结构,表明该链表有val(值)和next(指针)两个类型的属性
  • 即若定义了ListNode类,则可以使用l1.val取值,l1.next表示下一个指针的链接
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 * 看定义,想象成数据结构,表明该链表有val(值)和next(指针)两个类型的属性
 * 即若定义了ListNode类,则可以使用l1.val取值,l1.next表示下一个指针的链接
 */
 class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode *result = new ListNode;//结果链表  ×ListNode *next=NULL;
        ListNode *h=result;//???移动指针
        int sum=0;
        bool full=false;
        while(l1!=NULL || l2!=NULL)
        {
            //这里要一个一个判断是否为空,不然加不上
            if(l1!=NULL)
            {
                sum+=l1->val;
                l1=l1->next;
            }
            if(l2!=NULL)
            {
                sum+=l2->val;
                l2=l2->next;
            }
            if(sum>=10)
            {
                full=true;
                sum=sum%10;
            }
            else
            {
                full=false;//carry=sum>=10?true:false;
            }
            //a=a/10;//这里不要除10,他只是指向下一个字符,他不是int类型的!!没有个十百千
            //result->val=sum;
            
            //这里要指向新的  ×ListNode(val); h->next=new ListNode(1);等于直接插入1
            h->next=new ListNode(sum); //创建新的值为sum的链表
            h=h->next;
            sum=0;
            if(full)
            {
                sum=sum+1;
            }
        }
        if(sum)
        {
            h->next=new ListNode(1);//[8,9,9,9,0,0,0,1]最后一个1的情况
        }
        return result->next;
    }
};

3.无重复字符考点:(滑动窗口)

这个好难:知识点滑动窗口
【c++】力扣算法刷题 + 算法常用思想_第3张图片
链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters/solution/hua-dong-chuang-kou-by-powcai/

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
	vector<int> m(128,0);//这个容器为了存当遇到相同的字符(假设是a)时i应该变成的值,即前面那个a的下一个字符
	int ans=0; //最终的子串长度
	int i=0;
	for(int j=0;j<s.size();j++){
    	i=max(i,m[s[j]]);//如果遇到了相同的字符(假设为a),此时m[s[j]]会又去到同样的存储单元m[a的ASCII码值],因为之前遇到a时已经将这个位置的值改成前面那个a的下一个位置了,所以m[s[j]]大于i,将i更新
    	m[s[j]]=j+1;//更新这个位置的值,当下次再遇到该字母时,将i调整到该字母下一个位置的地方
    	ans=max(ans,j-i+1);//更新最终结果值,即没相同字母的子串的字符个数
}
return ans;
    }
};

//******法2
 if(s.size() == 0) return 0;
        unordered_set<char> lookup;
        int maxStr = 0;
        int left = 0;
        for(int i = 0; i < s.size(); i++){
            while (lookup.find(s[i]) != lookup.end()){
                lookup.erase(s[left]);
                left ++;
            }
            maxStr = max(maxStr,i-left+1);
            lookup.insert(s[i]);
    }
        return maxStr;
        
//我自己的,啥玩意啊就过去了:::这里应该注意maxnum和right的初始值问题
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int maxnum=0;
        int right,left=0;//i循环,j相当于下一次循环的开头?
        set<int> ss;
        set<int>::iterator it=ss.end();
        if(s.size()==0){
            return 0;
        }
        for(right=0;right<s.size();right++){
            it=ss.find(s[right]);
            if(it==ss.end()){   //如果该字符在字符表中未重复
                ss.insert(s[right]);
                maxnum=maxnum>ss.size()?maxnum:ss.size();
            }                
            else{
                ss.erase(s[left]); //移除该字符
                left++;            //该次小循环取消,进入下一个循环
                it=ss.find(s[right]);
                while(it!=ss.end())
                {
                    ss.erase(s[left]); //移除该字符
                    left++;
                    it=ss.find(s[right]);
                }
                ss.insert(s[right]);
            }
        }
        return maxnum;
    }
};

【c++】力扣算法刷题 + 算法常用思想_第4张图片

4.寻找两个正序数组的中位数***(考点:二分法)O(log (m+n))

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。

算法的时间复杂度应该为 O(log (m+n)) 。

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int leftLength = nums1.length;
        int rightLength = nums2.length;
        // 为了保证第一个数组比第二个数组小(或者相等)
        if (leftLength > rightLength) {
            return findMedianSortedArrays(nums2, nums1);
        }
        // 分割线左边的所有元素需要满足的个数 m + (n - m + 1) / 2;
        // 两个数组长度之和为偶数时,当在长度之和上+1时,由于整除是向下取整,所以不会改变结果
        // 两个数组长度之和为奇数时,按照分割线的左边比右边多一个元素的要求,此时在长度之和上+1,就会被2整除,会在原来的数
        //的基础上+1,于是多出来的那个1就是左边比右边多出来的一个元素
        int totalLeft = (leftLength + rightLength + 1) / 2;
        // 在 nums1 的区间 [0, leftLength] 里查找恰当的分割线,
        // 使得 nums1[i - 1] <= nums2[j] && nums2[j - 1] <= nums1[i]
        int left = 0;
        int right = leftLength;
        // nums1[i - 1] <= nums2[j]
        //  此处要求第一个数组中分割线的左边的值 不大于(小于等于) 第二个数组中分割线的右边的值
        // nums2[j - 1] <= nums1[i]
        //  此处要求第二个数组中分割线的左边的值 不大于(小于等于) 第一个数组中分割线的右边的值
        // 循环条件结束的条件为指针重合,即分割线已找到
        while (left < right) {
            // 二分查找,此处为取第一个数组中左右指针下标的中位数,决定起始位置
            // 此处+1首先是为了不出现死循环,即left永远小于right的情况
            // left和right最小差距是1,此时下面的计算结果如果不加1会出现i一直=left的情况,而+1之后i才会=right
            // 于是在left=i的时候可以破坏循环条件,其次下标+1还会保证下标不会越界,因为+1之后向上取整,保证了
            // i不会取到0值,即i-1不会小于0
            // 此时i也代表着在一个数组中左边的元素的个数
            int i = left + (right - left + 1) / 2;
            // 第一个数组中左边的元素个数确定后,用左边元素的总和-第一个数组中元素的总和=第二个元素中左边的元素的总和
            // 此时j就是第二个元素中左边的元素的个数
            int j = totalLeft - i;
            // 此处用了nums1[i - 1] <= nums2[j]的取反,当第一个数组中分割线的左边的值大于第二个数组中分割线的右边的值
            // 说明又指针应该左移,即-1
            if (nums1[i - 1] > nums2[j]) {
                // 下一轮搜索的区间 [left, i - 1]
                right = i - 1;
                // 此时说明条件满足,应当将左指针右移到i的位置,至于为什么是右移,请看i的定义
            } else {
                // 下一轮搜索的区间 [i, right]
                left = i;
            }
        }
        // 退出循环时left一定等于right,所以此时等于left和right都可以
        // 为什么left一定不会大于right?因为left=i。
        // 此时i代表分割线在第一个数组中所在的位置
        // nums1[i]为第一个数组中分割线右边的第一个值
        // nums[i-1]即第一个数组中分割线左边的第一个值
        int i = left;
        // 此时j代表分割线在第二个数组中的位置
        // nums2[j]为第一个数组中分割线右边的第一个值
        // nums2[j-1]即第一个数组中分割线左边的第一个值
        int j = totalLeft - i;
        // 当i=0时,说明第一个数组分割线左边没有值,为了不影响
        // nums1[i - 1] <= nums2[j] 和 Math.max(nums1LeftMax, nums2LeftMax)
        // 的判断,所以将它设置为int的最小值
        int nums1LeftMax = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
        // 等i=第一个数组的长度时,说明第一个数组分割线右边没有值,为了不影响
        // nums2[j - 1] <= nums1[i] 和 Math.min(nums1RightMin, nums2RightMin)
        // 的判断,所以将它设置为int的最大值
        int nums1RightMin = i == leftLength ? Integer.MAX_VALUE : nums1[i];
        // 当j=0时,说明第二个数组分割线左边没有值,为了不影响
        // nums2[j - 1] <= nums1[i] 和 Math.max(nums1LeftMax, nums2LeftMax)
        // 的判断,所以将它设置为int的最小值
        int nums2LeftMax = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
        // 等j=第二个数组的长度时,说明第二个数组分割线右边没有值,为了不影响
        // nums1[i - 1] <= nums2[j] 和 Math.min(nums1RightMin, nums2RightMin)
        // 的判断,所以将它设置为int的最大值
        int nums2RightMin = j == rightLength ? Integer.MAX_VALUE : nums2[j];
        // 如果两个数组的长度之和为奇数,直接返回两个数组在分割线左边的最大值即可
        if (((leftLength + rightLength) % 2) == 1) {
            return Math.max(nums1LeftMax, nums2LeftMax);
        } else {
            // 如果两个数组的长度之和为偶数,返回的是两个数组在左边的最大值和两个数组在右边的最小值的和的二分之一
            // 此处不能被向下取整,所以要强制转换为double类型
            return (double) ((Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin))) / 2;
        }
    }
}

5.最长回文子串(考点:动态规划、中心扩散)

中心扩散:遍历以每个字符为中心所能构成的最大字符串,但并不是所有字符串都有中心点(abba)朝两边扩散----------我想的是从外侧开始向内检查是否一样
动态规划其实就是空间换时间,避免重复计算,关键是找到初始状态和状态转移方程

class Solution {
public:
    pair<int,int> expandAroundCenter(const string& s,int left,int right){
        while(left>=0 && right<s.size() && s[left]==s[right])//right不能=,注意&&的顺序也是有讲究的满足范围写在前面
        {
            left--;
            right++;
        }
        return {left+1,right-1};//最后一趟是不满足的,要出来加上
    }

    string longestPalindrome(string s) {
        int num=s.size();
        int left,right=0;
        //if(num<=1||((num==2 && s[0]==s[1]))return s;//应该是返回空字符串,而不是0
        //if(num==2&&s[0]!=s[1])return s[0];
        for(int i=0;i<num;i++){
            auto [left1,right1]=expandAroundCenter(s,i,i);//单数情况
            auto [left2,right2]=expandAroundCenter(s,i,i+1);//双数情况
            //这里还要注意,第一次比较是与前一次的循环结果中最大的比较,还要第二次比较,不能直接返回
            //!!!!这只是一次循环,还有下次循环呢
            if(right1-left1>right-left){
                left=left1;
                right=right1;
                //return s.substr(left1,right1-left1+1);
            }
            if(right2-left2>right-left){//双和单比
                left=left2;
                right=right2;
                //return s.substr(left2,right2-left2+1);
            }
        }
        return s.substr(left,right-left+1);
    }
};

6.N 字形变换

想用二维数组岂不是很呆= =
不能限定define SIZE,不然后面为空,不好输出
最好还是找规律!!!本题flag很强,如图所示,res(string类型的)直接存放横着的数组,以方便输出
【c++】力扣算法刷题 + 算法常用思想_第5张图片

class Solution {
public://flag是碳基生物能想出来的吗
    string convert(string s, int numRows) {
        if(numRows<2)return s;//这里注意是numRows,比size的范围更大
        vector<string> res(numRows);//创建的行数是numRows行,每行相当于一个字符串进行存储
        int flag = -1;                          // 往上走还是往下走的标志
        int i=0;
        for(char c:s){
            res[i]+=c;//res[i].push_back(c);
            if (i == 0 || i == numRows - 1) {   // 行首行尾变向
                flag = -flag;                   //
            }                                   //
            i += flag;                          //
        }
        for(int i=1;i<numRows;i++){
            res[0]+=res[i];
        }
        return res[0];
    }
};

7. 整数反转

当比较时要观察是否数字已经改变了:本题的xBefore和xReverse

class Solution {
public:
    bool isPalindrome(int x) {
        if(x<0)return false;
        int xBefore=x;
        double xReverse;//这里用long long或者double以防数据溢出
        while(x!=0){
            xReverse=xReverse*10+x%10;//这里不要+=
            x=x/10;
        }
        //这里注意:当比较时要观察是否数字已经改变了:本题的x
        if(xBefore==xReverse){//return xReverse==x;
            return true;
        }else{
            return false;
        }

    }
};

8. 字符串转换整数 (atoi)

注意:flag来限制正负

class Solution {
public:
    int myAtoi(string s) {
        int flag=1;
        int sum=0,i=0;
        while(s[i++]==' ');//空格应该用‘’,注意什么时候放外面,这个反正不能放循环里面
        i--;
        if(s[i]=='+'){
            flag=1;
            ++i;
        }else if(s[i]=='-'){
            flag=-1;
            ++i;
        }
        for(;i<s.size();++i){//一个一个数来,这里注意不要让i=0了,之前已经定位了
            if(s[i]>='0'&&s[i]<='9'){
                if(sum>INT_MAX/10||sum==INT_MAX/10&&(s[i]-'0'>7)){
                    // 2147483640 + 8 cannot be represented in type 'int'
                    //我不懂为什么要>7而不是9
                    return (flag+1)?INT_MAX:INT_MIN;
                }
                sum=sum*10+(s[i]-'0');//注意+=和=好好想想吧
            }else{
                break;
            }
        }
        return sum*flag;
    }
};

9. 回文数

用long long或者double以防int的数据溢出

class Solution {
public:
    bool isPalindrome(int x) {
        if(x<0)return false;
        int xBefore=x;
        double xReverse;//这里用long long或者double以防数据溢出
        while(x!=0){
            xReverse=xReverse*10+x%10;//这里不要+=
            x=x/10;
        }
        //这里注意:当比较时要观察是否数字已经改变了:本题的x
        if(xBefore==xReverse){//return xReverse==x;
            return true;
        }else{
            return false;
        }

    }
};

10. 正则表达式匹配

11.盛最多水的容器

用while代替两个for的循环,没有必要两个循环,只要最大的测到就行
左右双指针移动找面积最大的(高按两个数中最小的算)

class Solution {
public:
    int maxArea(vector<int>& height) {
        /*map> h;
        for(int i=0;i
        int hsize=height.size();
        if(hsize<1)return -1;
        int s=0;
        int i=0,j=hsize-1;
        while(i<j)//用while代替两个for的循环,没有必要两个循环,只要最大的测到就行
        {
            int h=min(height[i],height[j]);
            s=max(s,h*(j-i));
            if(height[i]<height[j])++i;
            else --j;
        }
        return s;
        /*for(int i=0;i
    }
};

12. 整数转罗马数字

【c++】力扣算法刷题 + 算法常用思想_第6张图片
思想,从大到小排列,变成字典取里面的字符

class Solution {
public:
    string intToRoman(int num) {
        int a[] ={  1000,900,500,400,100, 90,  50, 40,  10,  9,  5,   4,   1};//把特殊情况的加进去
        string b[]={"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"};
        string Roman;
        int i=0;
        for(int i=0;i<13;i++){
            while(num>=a[i]){//注意等于号
                num-=a[i];
                Roman+=b[i];
            }
        }
        return Roman;
    }
};

14. 最长公共前缀

思想:把数组进行排序,这时候只需要看第一个字符串和最后一个字符串的公共前缀了,相当于夹逼

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        // 首先理解了字典序概念以后,把数组进行排序,这时候只需要看第一个字符串和最后一个字符串的公共前缀了,相当于夹逼
        string s="";
        sort(strs.begin(),strs.end());
        //用string类型方便数据下标的存取(v[i]只是代表的第几个)
        string a=strs.front(); //第一个元素
        string b=strs.back();  //最后一个元素
        /*
        for(int i=0;i
        int i=0;
        while(i<a.size() && i<b.size() && a[i] == b[i]){
            ++i;
        }
        return string(a.begin(),a.begin()+i);
    }
};

15. 三数之和:考点双指针

不重复的三元组。
i是循环,每次left和right动,不就可以得到所有3个数相加的情况了吗
【c++】力扣算法刷题 + 算法常用思想_第7张图片

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        int left=0,right=0;
        sort(nums.begin(),nums.end());
        //如果第一个数大于0,则不存在,注意放到循环(=0时也可以,三个0)
        vector<vector<int>> result;
        for(int i=0;i<nums.size();i++){
            if(nums[i]>0) return result; 
            //去重指的是i和left,right不能一样。???当起始的值等于前一个元素,那么得到的结果将会和前一次相同
            //nums[i]==nums[i+1]就相当于把相等的直接pass掉了,应该留一个的,a去重
            if(i>0 && nums[i]==nums[i-1]){
                continue;//跳出本次循环(即后移)
            }
            left=i+1;
            right=nums.size()-1;
            while(left<right){
                //这个思想,i是循环,每次left和right动,不就可以得到所有3个数相加的情况了吗
                if(nums[i]+nums[left]+nums[right]>0) --right;
                else if(nums[i]+nums[left]+nums[right]<0) ++left;
                else{
                    //注意输出的类型!!result.push_back(nums[i],nums[left],nums[right]);
                    result.push_back(vector<int>{nums[i],nums[left],nums[right]});
                    //当i相同时,当起始的值等于前一个元素,那么得到的结果将会和前一次相同(-2,-1,-1,3其中-1相同)
                    //注意这里的left是+还是-(已经进去了,避免比较下一次)
                    while(left<right && nums[left]==nums[left+1])left++;
                    while(left<right && nums[right]==nums[right-1])right--;
                    //找到答案,双指针应该同时收缩!!!!
                    --right;
                    ++left;
                }
            }
        }
        return result;
    }
};

16. 最接近的三数之和:考点双指针,abs

【c++】力扣算法刷题 + 算法常用思想_第8张图片

17. 电话号码的字母组合:考点回溯思想(想象成二叉树回溯思考)

注意这里的string letter=letterMap[a];取出数字对应的字符串,再用string[i]将其一个一个取出
【c++】力扣算法刷题 + 算法常用思想_第9张图片

class Solution {
private:
    const string letterMap[10] = {
    "", // 0
    "", // 1
    "abc", // 2
    "def", // 3
    "ghi", // 4
    "jkl", // 5
    "mno", // 6
    "pqrs", // 7
    "tuv", // 8
    "wxyz", // 9
};
    vector<string> result;
    string path;//有没有必要vector,还是string??
    void backtracking(string digits,int index){
        //或者index==
        if(path.size()==digits.size()){
            result.push_back(path);
            return;
        }

        int a=digits[index]-'0';//对应的数字(即下标2)
        //对应的字符串取出来(数字对应字符集)
        string letter=letterMap[a];
        //这里输出的个数要看(横着的大小!!),即字符串的长度
        for(int i=0;i<letter.size();i++){
            //path.push_back(letterMap[i][index]);
            path.push_back(letter[i]);
            backtracking(digits,index+1);
            path.pop_back();
        }
    }
public:
    vector<string> letterCombinations(string digits) {
        if(digits.size()==0){
            return result;
        }
        backtracking(digits,0);
        return result;
    }
};

18.四数之和

[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1],
[-1,-2,1,2],[-1,-1,0,2],[0,-2,0,2],[0,-1,0,1],[0,-2,0,2],[0,-1,0,1],[1,-2,-1,2],[1,-2,0,1],[1,-1,0,0],[2,-2,-1,1],[2,-2,0,0]] 重复
1.顺序重新拍了
2.数值重复取了
第一次修改,加上
【c++】力扣算法刷题 + 算法常用思想_第10张图片

[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1],
[-1,-2,1,2],[-1,-1,0,2],[0,-2,0,2],[0,-1,0,1],[1,-2,-1,2],[1,-2,0,1],[1,-1,0,0],[2,-2,-1,1],[2,-2,0,0]]
其中[0,-2,0,2],[0,-1,0,1],少了一次

第二次修改,加上
【c++】力扣算法刷题 + 算法常用思想_第11张图片
[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]
,[-1,-1,0,2],[0,-1,0,1],[1,-1,0,0]] 现在还剩数字重复取值的情况

二、算法常用基本思想

1.双指针

双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
见15,三数之和,11,盛水容器。类似的还有四数之和,最接近的三数之和

如:合并k个已排序的链表,可以用1、2两种方法解决,时间复杂度 O(nlogn)
特殊情况:查找峰值
给定一个长度为n的数组nums,请你找到峰值并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个所在位置即可。
【c++】力扣算法刷题 + 算法常用思想_第12张图片
这里利用到了二分法:
【c++】力扣算法刷题 + 算法常用思想_第13张图片
这里要注意,如果是按照以前的移动left和right会导致越界或者死循环(动手写一下试试[4,2]和[3,6]两种情况)

2.分治:数组的归并排序

分治即“分而治之”,“分”指的是将一个大而复杂的问题划分成多个性质相同但是规模更小的子问题,子问题继续按照这样划分,直到问题可以被轻易解决;“治”指的是将子问题单独进行处理。经过分治后的子问题,需要将解进行合并才能得到原问题的解,因此整个分治过程经常用递归来实现。

数组的归并排序是分治思想,即将数组从中间个元素开始划分,然后将划分后的子数组作为一个要排序的数组,再将排好序的两个子数组合并成一个完整的有序数组,因此采用的是递归。

对于这k个链表,就相当于上述合并阶段的k个子问题,需要划分为链表数量更少的子问题,直到每一组合并时是两两合并,然后继续往上合并,这个过程基于递归:

  1. 终止条件: 划分的时候直到左右区间相等或左边大于右边。
  2. 返回值: 每级返回已经合并好的子问题链表。
  3. 本级任务:对半划分,将划分后的子问题合并成新的链表。

即:不断的对半划分,最后再合并

例1:合并两个—>合并区间---->list合并

【c++】力扣算法刷题 + 算法常用思想_第14张图片

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
  public:
    //时间复杂度 O(nlogn):双指针思想,分支思想(划分为小问题,一般用递归进行解决)
    ListNode* MergeTwo(ListNode* pHead1, ListNode* pHead2) {
        ListNode* dummy = new ListNode(-1);
        ListNode* node = dummy;

        //递归当一个为空时,直接返回另外一个
        if (pHead1 == nullptr) {
            return pHead2;
        }
        if (pHead2 == nullptr) {
            return pHead1;
        }

        while (pHead1 && pHead2) {
            if (pHead1->val < pHead2->val) {
                node->next = pHead1;
                pHead1 = pHead1->next;
            } else {
                node->next = pHead2;
                pHead2 = pHead2->next;
            }
            node = node->next;
        }
        //有一方为空时,直接连在后面
        node->next = pHead1 ? pHead1 : pHead2;
        return dummy->next;
    }

    //合并区间函数
    ListNode* divideMerge(vector<ListNode*>& lists,int left,int right){
        if(left>right)return nullptr;
        else if(left==right){
            return lists[left];
        };
        int mid=(left+right)/2;
        //while(left
        //MergeTwo里面是两个指针
        return MergeTwo(divideMerge(lists, left, mid),divideMerge(lists,mid+1,right));
    }

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        return divideMerge(lists,0,lists.size()-1);
    }
};


更快的方法:将所有的元素访问并加入vector容器,sort排序后加入新的链表中
【c++】力扣算法刷题 + 算法常用思想_第15张图片

例2:单链表排序

//2.利用分治思想:分段排序(不断调用自己),合并
    //合并两个有序链表
    ListNode* myMerge(ListNode* pHead1,ListNode* pHead2){
        if(pHead1==nullptr)
            return pHead2;
        if(pHead2==nullptr)
            return pHead1;
        ListNode* mergeList=new ListNode(-1);
        ListNode* cur=mergeList;
        while(pHead1!=nullptr && pHead2!=nullptr){
            if(pHead1->val >pHead2->val){
                cur->next=pHead2;
                pHead2=pHead2->next;
            }
            else{
                cur->next=pHead1;
                pHead1=pHead1->next;
            }
            cur=cur->next;
        }
        cur->next=(pHead1==nullptr)?pHead2:pHead1;
        return mergeList->next;
    }

    ListNode* sortInList(ListNode* head) {
        if (head==nullptr || head->next==nullptr) {
            return head;
        }
        ListNode* left=head;
        ListNode* mid=head->next;
        ListNode* right=head->next->next;
        while (right!=nullptr && right->next!=nullptr) {
            left=left->next;
            mid=mid->next;
            right=right->next->next;
        }
        //左边指针指向左段的左右一个节点,从这里断开
        left->next = nullptr;
        return myMerge(sortInList(head),sortInList(mid));
    }
};

例3:数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P mod 1000000007

数组(3,1,4,5,2)的逆序对有(3,1),(3,2),(4,2),(5,2),共4个。
0 1 2 3 4
【c++】力扣算法刷题 + 算法常用思想_第16张图片

#include 
class Solution {
  public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param nums int整型vector
     * @return int整型
     */
    int mod = 1000000007;
        int InversePairs(vector<int> data) {
        int n = data.size();
        vector<int> res(n);
        return mergeSort(0, n - 1, data, res);
    }

    int mergeSort(int left, int right, vector<int>& data, vector<int>& temp) {
        if (left >= right)
            return 0;
        int mid = (left + right) / 2;
        //分割
        int res = mergeSort(left,mid,data,temp)+mergeSort(mid+1,right,data,temp);
        //防止溢出
        res %= mod;
        int i = left, j = mid + 1;
        //临时存放
        for (int k = left; k <= right; k++)
            temp[k] = data[k];
        for (int k = left; k <= right; k++) {
            if (i == mid + 1)
                data[k] = temp[j++];
            else if (j == right + 1 || temp[i] <= temp[j])
                data[k] = temp[i++];
            //左边比右边大,答案增加
            else {
                //数据顺序不对
                data[k] = temp[j++];
                //统计逆序对
                res += mid - i + 1;
            }
        }
        return res % mod;
    }
};
//法一:暴力超时
/*int InversePairs(vector& nums) {
    int p = 0;
    int ret = 0;
    int n = nums.size();
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            if (nums[i] < nums[j]) {
                continue;
            } else {
                p++;
                ret = p % 1000000007;
            }
        }
    }
    return ret;
}*/

3.堆栈队列

(1)大顶堆、小顶堆

//小顶堆,升序队列
priority_queue <int,vector<int>,greater<int>> pri_que;
//大顶堆,降序队列
priority_queue <int,vector<int>,less<int>> pri_que;
//默认大顶堆
priority_queue<int> pri_que;

大顶堆:根最大(从大到小排);小顶堆:根最小(从小到大排)

处理前k个高频、低频结果的操作!!!!(堆遍历,里面维持K个元素)

以value为基准做从小到大的排序

堆pop,是从堆顶弹出的

如果在传统的排序算法中(例如冒泡,插入等),我们习惯把目标数据整体进行一次排序,再截取出前1000个最小的或者最大的。当不需要知道它们的排序顺序,只需要知道它们都比我们1000个目标数据中最大值都要大,这就是堆排序
它的思想整体上非常清晰简单,结构上是一个完全二叉树
根结点比左右孩子都大则叫做大根堆。其中,左右孩子的根结点,又会比它们的左右孩子都大。
根结点比左右孩子都小则叫做小根堆。其中,左右孩子的根结点,又会比它们的左右孩子都小。
更简单点说:所有中间结点,一定比它孩子大(大根堆),或者一定比它的孩子小(小根堆)
相关解释见:堆排序(大根堆与小根堆)

priority_queue 是一个优先队列容器,它是一个模板类,可以存储任何类型的数据。它可以自动将元素按照指定的排序规则进行排序,然后在队列的顶部放置最高优先级的元素。priority_queue 通常用于需要按照优先级进行处理的应用程序,例如 Dijkstra 算法、贪心算法等。要使用 priority_queue,需要包含头文件 。可以通过指定比较器来自定义元素排序规则,也可以使用默认的 less 比较器进行排序。priority_queue 有 push、pop、top、empty、size 等成员函数,可以方便地对队列进行操作。
具体详见:
C++ 优先队列 priority_queue 使用篇
优先级队列: 搞懂大顶堆和小顶堆 c++

例1 最小的K个数(需要vector进行存储)

给定一个长度为 n 的可能有重复值的数组,找出其中不去重的最小的 k 个数。例如数组元素是4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4(任意顺序皆可)。

如下所示,当新的数字小于栈顶,则新数字入队
【c++】力扣算法刷题 + 算法常用思想_第17张图片
时间复杂度:O(nlongk), 插入容量为k的大根堆时间复杂度为O(longk), 一共遍历n个元素
空间复杂度:O(k)

例2 第K个大的数(其实也可以用priority_queue

有一个整数数组,请你根据快速排序的思路,找出数组中第 k 大的数。
给定一个整数数组 a ,同时给定它的大小n和要找的 k ,请返回第 k 大的数(包括重复的元素,不用去重),保证答案存在。

4.贪心算法

局部最优->全局最优
如:取钞票
例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?
指定每次拿最大的,最终结果就是拿走最大数额的钱。
每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。
手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。

例1:分发饼干
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

局部最优:大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个

全局最优:喂饱尽可能多的小孩

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int index = s.size() - 1; // 饼干数组的下标
        int result = 0;
        for (int i = g.size() - 1; i >= 0; i--) { // 遍历胃口
            if (index >= 0 && s[index] >= g[i]) { // 遍历饼干
                result++;
                index--;
            }
        }
        return result;
    }
};

时间复杂度:O(nlogn)
空间复杂度:O(1)

类似还有股票问题,跳跃游戏,加油站,柠檬水找零,最少数量的箭燃爆气球,区间问题等。

例2:股票问题
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)

假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。
相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。
此时就是把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int result = 0;
        for (int i = 1; i < prices.size(); i++) {
            result += max(prices[i] - prices[i - 1], 0);
        }
        return result;
    }
};

时间复杂度:O(n)
空间复杂度:O(1)

例3:合并区间
给出一组区间,区间包括起始点,要求将重叠的区间合并
重叠后的区间按照起点位置升序排列

/**
 * struct Interval {
 *	int start;
 *	int end;
 *	Interval(int s, int e) : start(start), end(e) {}
 * };
 * //其实定义为vector>更方便
 */

5.回溯

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

组合:子集问题,集合无序的,取过的元素不会重复取,写回溯,for从startIndex开始

排列:for从0开始,因为排列问题中集合是有序的(可以重复选取)

​ 之前组合,[1,2,3]取了1之后,2只能取3.

​ 现在,[1,2,3]取了1之后,2还能取1,3(2的used数组是1不能取),因此集合从0开始

例1:字符串的排列
树枝:处理输入ab,输出aa的情况
【c++】力扣算法刷题 + 算法常用思想_第18张图片
例2:N皇后问题

class Solution {
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
void backtracking(int n, int row, vector<string>& chessboard) {
    if (row == n) {
        result.push_back(chessboard);
        return;
    }
    for (int col = 0; col < n; col++) {
        if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
            chessboard[row][col] = 'Q'; // 放置皇后
            backtracking(n, row + 1, chessboard);
            chessboard[row][col] = '.'; // 回溯,撤销皇后
        }
    }
}
bool isValid(int row, int col, vector<string>& chessboard, int n) {
    // 检查列
    for (int i = 0; i < row; i++) { // 这是一个剪枝
        if (chessboard[i][col] == 'Q') {
            return false;
        }
    }
    // 检查 45度角是否有皇后
    for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
        if (chessboard[i][j] == 'Q') {
            return false;
        }
    }
    // 检查 135度角是否有皇后
    for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
        if (chessboard[i][j] == 'Q') {
            return false;
        }
    }
    return true;
}
public:
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        std::vector<std::string> chessboard(n, std::string(n, '.'));
        backtracking(n, 0, chessboard);
        return result;
    }
};

你可能感兴趣的:(c++,leetcode,c++,算法)