Leetcode刷题学习记录

力扣

分类

算法

滑动窗口: 3, 209, 1456

动态规划: 5, 70, 322

中心扩散: 5

双指针: 11, 27, 206

递归: 21, 70, 206

分治: 50, 215

回溯: 22, 46, 77, 78

dfs: 22, 200, 322, 547

二分查找: 35, 374

贪心: 53

二进制: 78

并查集: 200, 547

排序: [215](#215. 数组中的第K个最大元素)

数据结构

hashMap: 1, 3, 705

array: 27

link: 2, 203

statck: 20, 235

queue: [225](#225. 用队列实现栈), 622

set: 217

1.两数之和

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

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

你可以按任意顺序返回答案。

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

解法1.暴力解法

/*
    57 / 57 个通过测试用例
    状态:通过
    执行用时: 460 ms
    内存消耗: 9.9 MB
    
    时间复杂度O(n^2)
	此时还没意识到hash表能降低时间复杂度
*/
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        vector<int> a;
        for (int i = 0; i < nums.size()-1; i++)
        {
            for (int j = i+1; j < nums.size(); j++)
            {
                if (nums[i]+nums[j] == target && i != j)
                {
                    a.push_back(i);
                    a.push_back(j);
                }
            }
        }
        return a;
    }
};

解法2.hashMap

/*
	57 / 57 个通过测试用例
    状态:通过
    执行用时: 12 ms
    内存消耗: 10.9 MB
    
    时间复杂度O(n)
*/
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        map<int, int> m;
        for (int i = 0; i < nums.size(); i++)
        {
            //不适用m[nums[i]] = i; 的形式 这样就成了唯一key,不符合题意,可能答案给的target由两个值相同但不同下标的数相加而成
            m.insert(pair<int, int>(nums[i], i));	//将自己插入map中
            auto iter = m.find(target - nums[i]);	//以target和当前所指数的差值作为key, 查询map中是否含有.
            if (iter != m.end() && i != iter->second)
            {
                return {iter->second, i};	//有则返回
            }
        }
        return {};
    }
};

知识点

数据结构

熟悉了解各种哈希表
std的各种自带数据结构: map, hash_map, multi_map, unorder_map
且map不等于set, map可以是不唯一key,像这道题有可能出现两个相同的数相加成一个结果,set无法做到

这道题不需要考虑顺序,所以用unordered_map会更好,查找的时间复杂度为O(1)

2.两数相加

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

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

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

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

解法1.链表的基本操作

/**
 * 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) {}
 * };
 */
/*
    1568 / 1568 个通过测试用例
    状态:通过
    执行用时: 24 ms
    内存消耗: 69.4 MB
*/
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* retNode = new ListNode();
        ListNode *pRet = retNode;
        ListNode *p1 = l1;
        ListNode *p2 = l2;
        int num = 0;
        bool flag = false;  //进1标志
        bool first = true;
        while (p1 || p2)
        {
            num = (p1 ? p1->val : 0) + (p2 ? p2->val : 0) + (flag ? 1 : 0);
            flag = false;
            if (!first)
            {
                pRet->next = new ListNode();
                pRet = pRet->next;
            }
            first = false;
            if (num >= 10)
            {
                num -= 10;
                flag = true;
            }
            pRet->val = num;

            p1 = p1 ? p1->next : NULL;
            p2 = p2 ? p2->next : NULL;
        }
        if (flag)
        {
            pRet->next = new ListNode();
            pRet = pRet->next;
            pRet->val = 1;
        }
        return retNode;
    }
};

解法1优化

/*
	优化了下,没引入p1, p2去遍历链表, 且返回的是retNode的next,就可以不引入first变量去判断了.
	且用 % 操作去代替了 - 操作,就不用判断num >= 10才num-=10了
	1568 / 1568 个通过测试用例
    状态:通过
    执行用时: 24 ms
    内存消耗: 69.5 MB
*/
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* retNode = new ListNode();
        ListNode *pRet = retNode;
        int num = 0;
        bool flag = false;  //进1?
        while (l1 || l2)
        {
            pRet->next = new ListNode();
            pRet = pRet->next;

            num = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + (flag ? 1 : 0);
            flag = (num / 10) ? true : false;	//*
            num %= 10;	//*

            l1 = l1 ? l1->next : NULL;
            l2 = l2 ? l2->next : NULL;
            pRet->val = num;
        }
        if (flag)
        {
            pRet->next = new ListNode();
            pRet = pRet->next;
            pRet->val = 1;
        }
        return retNode->next;	//*
    }
};

知识点

数据结构

链表

3.无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:

输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。

解法1.暴力解法

/*
    987 / 987 个通过测试用例
    状态:通过
    执行用时: 856 ms
    内存消耗: 258 MB
    
    时间复杂度O(n^2)
*/
/*
	熟悉set,unordered_set
*/
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int maxLength = 0;
        for (int i = 0; i < s.length(); i++){
            set<char> record;
            for (int j = i; j < s.length(); j++){
                if (record.find(s[j]) == record.end()){	//两层for, 寻找以i开始,j结尾的子串,记录最大长度.
                    record.insert(s[j]);
                    maxLength = maxLength > record.size() ? maxLength : record.size();
                } else {
                    break;
                }
            }
        }
        return maxLength;
    }
};

解法2.滑动窗口

/*
	解法: 滑动窗口
	适用于子串相关操作
	
	了解了hash_map,底层原理是利用桶(下标范围很大的数组,下标意义为key经过hash变换后的值),空间换时间的思想.
*/
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        map<int, int> rec;
        int maxLength = 0;
        int start = 0;
        for (int idx = 0; idx < s.length(); idx++){
            //int pos = rec[s[idx]]; //不知道为什么之前访问map会插入到map中,尽量还是用rec.find(s[idx])->second的形式吧
            if (rec.find(s[idx]) != rec.end()){	//当idx指向的字符已在hash表中
                start = max(start, rec.find(s[idx])->second + 1);	//则将start指向map中存储该字符的下一个位置
            }
            rec[s[idx]] = idx;
            maxLength = max(maxLength, idx-start+1);
        }
        return maxLength;
    }
};

知识点

算法

滑动窗口

数据结构

Map

4.寻找两个正序数组的中位数(x)

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

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

解法1.暴力解法

/*
    2094 / 2094 个通过测试用例
    状态:通过
    执行用时: 28 ms
    内存消耗: 87 MB
    
	暴力解法,不符题意
*/
class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        nums1.insert(nums1.end(), nums2.begin(), nums2.end());
        sort(nums1.begin(), nums1.end());
        if (nums1.size() % 2 == 0)
        {
            int n1 = nums1[((1 + (nums1.size() - 1)) / 2) - 1];
            int n2 = nums1[((1 + (nums1.size() + 1)) / 2) - 1];
            return (n1+n2)/2.0;
        }
        return nums1[((1 + nums1.size()) / 2) - 1];
    }
};

5.最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:

输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。

解法1.中心扩散

/*
	180 / 180 个通过测试用例
    状态:通过
    执行用时: 12 ms
    内存消耗: 6.9 MB
*/
class Solution {
public:
    int centerExpand(string &s, int start, int end)
    {
        //左指针到头 or 右指针到尾 or 两指针指向的值不同则返回
        while(start >=0 && end < s.length() && s[start] == s[end]) {
            start--;
            end++;
        }
        return end - start - 1;
    }

    string longestPalindrome(string s) {
        if (s.length() <= 1)
        {
            return s;
        }
        int center = 0;
        int maxLength = 0;
        for (int i = 0; i < s.length() - 1; i++)    //i为扩散中心下标
        {
            int len1 = centerExpand(s, i, i);   //单中心情况
            int len2 = centerExpand(s, i, i+1); //双中心情况
            if (len1 > maxLength || len2 > maxLength)
            {
                maxLength = max(len1, len2);
                center = i;
            }
        }
        if (maxLength % 2 == 0)
        {
            return s.substr(center - (maxLength/2) + 1, maxLength);
        }
        return s.substr(center - (maxLength-1)/2, maxLength);
    }
};

解法2.动态规划

/*
	解法: 动态规划
    时间复杂度:O(n^2),其中 n 是字符串的长度。动态规划的状态总数为 O(n^2),对于每个状态,我们需要转移的时间为 O(1)。
    空间复杂度:O(n^2),即存储动态规划状态需要的空间。
    
    用空间换时间, 用一个数组来保存前个状态的结果,就不需要再重新再计算判断一遍了, 动态规划最重要的思想就是利用上一个状态
*/

#include <iostream>
#include <string>
#include <vector>

using namespace std;

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        if (n < 2) {
            return s;
        }

        int maxLen = 1;
        int begin = 0;
        // dp[i][j] 表示 s[i..j] 是否是回文串
        vector<vector<int>> dp(n, vector<int>(n));
        // 初始化:所有长度为 1 的子串都是回文串
        for (int i = 0; i < n; i++) {
            dp[i][i] = true;
        }
        // 递推开始
        // 先枚举子串长度
        for (int L = 2; L <= n; L++) {
            // 枚举左边界,左边界的上限设置可以宽松一些
            for (int i = 0; i < n; i++) {
                // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
                int j = L + i - 1;
                // 如果右边界越界,就可以退出当前循环
                if (j >= n) {
                    break;
                }

                if (s[i] != s[j]) {
                    dp[i][j] = false;
                } else {
                    if (j - i < 3) {	//j - i + 1 <= 3
                        dp[i][j] = true;
                    } else {	//看状态转移方程的上一状态是否为true
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }

                // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
                if (dp[i][j] && j - i + 1 > maxLen) {
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substr(begin, maxLen);
    }
};

Alchemist (alchemist-al.com)

知识点

算法

动态规划

11.盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

输入:[1,8,6,2,5,4,8,3,7]
输出:49

解法1.暴力算法

/*
	时间复杂度O(n^2)
	两层循环一个遍历左边界,一个遍历右边界
*/
class Solution {
public:	
    int maxArea(vector<int>& height) {
        int maxArea = 0;
        int size = height.size();
         for (int left = 0; left <= size - 1; left++){
             for (int right = left+1; right < size; right++){
                 maxArea = max(maxArea, min(height[left], height[right]) * (right - left));
             }
         }
         return maxArea;
    }
};

解法2.双指针

/*
    60 / 60 个通过测试用例
    状态:通过
    执行用时: 84 ms
    内存消耗: 57.7 MB
    
    时间复杂度 O(n)
    左右指针分别指向头尾, 面积S=底*高, 此时底最大, 高为两边高最短的一边.
    两指针开始忘中间靠拢,底会变小,所以舍弃高度更小的一边,才有可能使面积变大.
*/

class Solution {
public:
    int maxArea(vector<int>& height) {
        int maxArea = 0;
        int right = height.size()-1;
        int left = 0;
         while (right > left){
                maxArea = max(maxArea, min(height[left], height[right]) * (right - left));
                if (height[right] > height[left]){
                    left++;
                } else {
                    right--;
                }
         }
         return maxArea;
    }
};

知识点

算法

双指针, 双指针大多都是对两层循环的优化, 所以当暴力法涉及到两层循环遍历的时候, 我们就应该有这种思想: 能不能用到双指针的思想.

20.有效的括号

解法1.栈

/*
	括号匹配是使用栈解决的经典问题。
	由于栈结构的特殊性,非常适合做对称匹配类的题目。
	
	如果只是判断单括号的话可以用一个整型balance去记录, 依次读取括号, 读取到'('则++, 读取到 ')'则--,
	当出现balance < 0的情况,说明右括号比左括号多,不匹配,则返回, 最后如果balance == 0,则有效.
*/
class Solution {
public:
    char getRight(char ch)
    {
        switch (ch) {
            case '(':	return ')';
            case '[':	return ']';
            case '{':	return '}';
        }
        return '\0';
    }

    bool isValid(string s) {
        if (s.length() % 2 != 0) {
            return false;
        }

        //左括号入栈, 右括号出栈, 出栈失败则无效
        stack<int> st;
        for (int i = 0; i < s.length(); i++) {
            if (getRight(s[i]) == '\0') {	//right
                if (st.empty() || getRight(st.top()) != s[i]) {
                    return false;
                }
                st.pop();
            } else {
                st.push(s[i]);
            }
        }
        return (st.empty());
    }
};

知识点

数据结构

21. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

解法1.常规思路

/**
 * 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) {}
 * };
 */
/*
    208 / 208 个通过测试用例
    状态:通过
    执行用时: 8 ms
    内存消耗: 14.5 MB
    
    暴力解法,引入一个新的链表,分别两个指针指向各自的链表的节点, 比较大小,谁比较小就取谁的值,并且那个链表对应的指针指向下一个节点.
    时间复杂度 O(m+n)
    空间复杂度 O(m+n)
*/
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode* retLink = new ListNode(0);
        ListNode* cur = retLink;
        while (list1 || list2) {
            if (!list2 || (list1 && list1->val <= list2->val)){	//list2到末尾或者l1 < l2
                cur->next = new ListNode(list1->val);
                list1 = list1->next;
            } else {	//list1到末尾或者 l2 < l1
                cur->next = new ListNode(list2->val);
                list2 = list2->next;
            }
            cur = cur->next;
        }
        return retLink->next;
    }
};

解法2.递归

递归思想,一生之敌

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (l1 == NULL) {	//l1空,则连接剩下的l2
            return l2;
        }
        if (l2 == NULL) {	//反之亦然
            return l1;
        }
        if (l1->val <= l2->val) {
            l1->next = mergeTwoLists(l1->next, l2);	//l1指向较小的节点
            return l1;	//将l1返回给上层递归
        }
        l2->next = mergeTwoLists(l1, l2->next);
        return l2;
    }
};

知识点

算法

递归

22.括号生成

解法1.回溯+dfs

class Solution {
public:
    vector<string> generateParenthesis(int n) {
        vector<string> ret;
        string str {""};

        dfs(ret, str, n, n);		//n对括号匹配, 有n个 '(' 与 n个 ')'
        return ret;
    }

    void dfs(vector<string> &ret, string cur, int n1, int n2) {
        if (n1 == 0 && n2 == 0) {
            ret.push_back(cur);		//括号全有效地放置完, 放到ret中
            return;
        }

        if (n1 > n2) {				//无效括号匹配
            return;
        }
        if (n1 > 0) {
            cur += "(";
            n1--;
            dfs(ret, cur, n1, n2);
            //n1 < n2, 即当前的 '(' > ')', 下一个可以是 ')', 则状态回溯, 进入if (n2 > 0) {...}去判断, 否则n1 == n2, 则下一个只能放 '('
            if (n1 != n2) {
                cur.pop_back();
                n1++;
            }
        }
        
        if (n2 > 0) {
            cur += ")";
            n2--;
            dfs(ret, cur, n1, n2);
        }
    }
};

回溯的灵魂就是画出树结构图
现这其实就是一个满二叉树,我们只需要DFS所有节点即可,只不过有一些状态可以提前返回。

知识点

算法

回溯

27.移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

解法1.双指针?

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int n = nums.size();
        int len = 0;
        for (int i = n-1; i >= 0; i--) {
            if (val == nums[i]) {
                int tmp = nums[n-len-1];
                //nums[n-len-1] = val;  根据题意[不需要考虑数组中超出新长度后面的元素], 所以没啥必要
                nums[i] = tmp;
                len++;
            }
        }
         return n - len;
    }
};

35.搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。

解法1.二分查找

/*
	时间复杂度O(log n),因为会每次遍历都会舍弃一半的数
	空间复杂度O(1)
*/
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0;		//左边界指向头
        int right = n - 1;	//右边界指向尾
        while(left <= right) {	//当左指针在右指针的右边时,跳出循环
            int mid = ((right - left) << 1) + left	// left + (right-left)/2 或者 (right + left) / 2;
            if (nums[mid] == target) {			//找到了, 返回
                returen mid;
            } else if (nums[mid] < target) {	//目标在边界中心右边, 那中心应该右移, 所以左指针直接移去中心+1处
                left = mid + 1;
            } else {
                right = mid - 1;				//含义上同
            }
        }
        return left;
    }
};

这是一道非常经典的二分查找的题,上面也是非常标准的二分查找模板

46.全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

解法1.回溯

class Solution {
public:

    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> ret;
        vector<int> path;
        backtrack(nums, path, ret);
        return ret;
    }

    void backtrack(const vector<int>& nums, vector<int> &path, vector<vector<int>> &ret) {
        if (path.size() == nums.size()) {	//递归出口
            ret.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (isInPath(path, nums[i])) {	//剪枝: 若元素已经在path内,则跳过
                continue;
            }
            path.push_back(nums[i]);		//选择
            backtrack(nums, path, ret);		//递归
            path.pop_back();				//撤销选择
        }
    }

    bool isInPath(vector<int> &path, int num) {
        for (int i = 0; i < path.size(); i++) {
            if (num == path[i]) {
                return true;
            }
        }
        return false;
    }
};
画出树 eg:nums = {1,2,3}
                        1              2                3
                      2   3          1   3            1   2
                    3       2      3       1        2       1
不难看出, 横向遍历为1~3, 纵向递归也是1~3, 不需要状态变量控制, 只不过需要剪枝, 把元素push到路径中有条件: 路径中不存在当前元素

50.实现Pow(x, n)

实现 pow(x, n) ,即计算 xn 次幂函数(即,xn )。

解法1.分治

/*
	0 ms	5.9 MB
	时间复杂度O(logn), 因为每次都是折半计算
*/
class Solution {
public:
    double myPow(double x, int n) {
        return calc(x, n);  //计算x^n
    }

    double calc(double x, long long int m) {
        if (m < 0) {    //负数特殊处理
            x = 1/x;
            m = -m;     //防那个sb特殊用例导致溢出
        } else if (m == 0) {
            return 1.0; //递归出口
        }
        
        double ret = 1.0;
        ret = calc(x, m/2);      //若是奇数则向下取整, 等下得乘回去
        ret *= ret;              //结合上一步, x^m == [x^(m/2)] * [x^(m/2)], 少计算一半
        ret *= m & 1 ? x : 1;    //嗯, 乘回去
        return ret;
    }
};

举个栗子:

​ 计算3的9次方步骤:

  1. 3^9 = [3333] [3333] [3], 计算3的4(9//2)次方得出ret, 得出的ret再乘一遍, 这样就不必把第二部分重复的[3333]再算一遍了, 最后就是把落单的单数乘回去即可
  2. 接着计算[3333] = [33] [33] [], 上同.
  3. [33] = [3^1] [3^1] []
  4. [3^1] = [] [] [3], 因为1//2 == 0, 所以前两部分是1, 第三部分的[3]是单数剩余的, 所以结果正确.

53.最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

解法1.运用数学关系(原来是贪心算法的思想)

/*
    209 / 209 个通过测试用例
    状态:通过
    执行用时: 92 ms
    内存消耗: 66.1 MB
    
    时间复杂度O(n)
    rule:
	1.不可能负数开始
    2.若与nums[i]相加后为负数,则从i+1再开始记录, 不过也要对比当前的maxSum, 因为有可能全是负数.
*/
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int start = 0;
        int curSum = 0;
        int maxSum = nums[0];

        for (int i = 0; i < nums.size(); i++) {
            if (curSum + nums[i] > 0) {
                curSum += nums[i];
                maxSum = max(maxSum, curSum);
            } else {
                start = i + 1;
                curSum = 0;
                maxSum = max(maxSum, nums[i]);
            }
        }
        return maxSum;
    }
};

知识点

算法

贪心算法

70.爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。

  1. 1 阶 + 1 阶 + 1 阶
  2. 1 阶 + 2 阶
  3. 2 阶 + 1 阶

解法1.排列组合

/*
	执行错误: 上了30左右楼层后, 计算排列的时候 计算返回的结果超过int上限
	思路: 找出规律 (x:走多少个一步,y:走多少个两步)
	1.只需要知道有n种2x+y的个数即可.之后再用组合C计算出每种2x+y的组合方式
	2.先从最大的两步开始迭代
		例如5步:
			x/y				组合个数
			x:2, y:1	->	C23	三个位置中 x可以有多少种组合方式
			x:1, y:3	->	C14
			x:0, y:5	->	C05
	3.如何得到最大的两步数?
		max = n % 2 == 0 ? n/2 : (n-1) / 2;
		等价于
		max = int(n/2)
*/
class Solution {
public:
    int calStepTimes(int n, int twoStep)
    {
        int oneStep = n - (2*twoStep);
        return C(oneStep, (oneStep+twoStep));   //计算组合 Cmn 含义是 n个位置中, m有多少种组合方式
    }
    
    int C(int m, int n)	//C^m_n = A^m_n / A^m_m
    {
        if (m >= n/2){  //Cmn == C(n-m)n, 减少循环次数
            m = n - m;
        }
        return int(A(m, n, 0) / A(m, m, 0));
    }

    int A(int m, int n, int cur)	//A^m_n, cur为当前递归的层数
    {
        if (cur == m){
            return 1;
        }
        return n * A(m, n-1, ++cur);
    }
    
    int climbStairs(int n) {
        int times = 0;
        int twoStep = n / 2;  	//多少个两台阶
        while (twoStep >= 0){	//从最大两步开始开始迭代
            times += calStepTimes(n, twoStep);
            twoStep--;
        }
        return times;
    }
};

解法1优化

/*
	根据xwx的约分思路,乘除的操作交叉进行,让结果尽量小,不超出int上限.
	除此之外,使用组合C的另一个公式
	     以 C(2, 5) 为例:
 *               5!    
 *          ------------
 *          (5-2)! * 2!
 *
 *                   ↓
 *            5*4*(3*2*1)
 *         ----------------
 *          (3*2*1) * 2*1
 *             ↑
 *         先约掉大的公约数
 *             
 *               5*4
 *             -------
 *               2*1
	最后按顺序计算以保证数据不会溢出: *4 /1 *5 /2
	按这个顺序能让每个被除数除尽
	分子由最大公约数+1开始,就是上面的4,到n结束
	分母由1开始,到m结束, 分子和分母进行上述约分后数量是相等的.
*/
class Solution {
public:
    int C(int m, int n)	//C^m_n = n! / m! / (n-m)! 
    {
        if (m > n/2){  //Cmn == C(n-m)n, 减少循环次数且将m变成最大公约数
            m = n - m;
        }

        long ret = 1;
        int i = n - m + 1, j = 1;
        for (; j <= m; i++, j++){    //相当于约掉公约数
            ret *= i;
            ret /= j;
        }
        return static_cast<int>(ret);
    }
    
    int climbStairs(int n) {
        int times = 0;
        int twoStep = n / 2;  	//多少个两台阶
        int oneStep;
        while (twoStep >= 0){	//从最大两步开始开始迭代
            oneStep = n - (2*twoStep);
            times += C(oneStep, (oneStep+twoStep));   //计算组合 Cmn 含义是 n个位置中, m有多少种组合方式;
            twoStep--;
        }
        return times;
    }
};

解法2.暴力递归(斐波那契)

/*
	时间复杂度O(2^n)
	其中会有很多重复计算,执行超时
	这就是一个斐波那契数列
	递归 自顶向下
*/

class Solution {
public:
    int climbStairs(int n) {
        if (n == 1) { return 1; }
        if (n == 2) { return 2; }
        return climbStairs(n-1) + climbStairs(n-2);
    }
};

解法3.动态规划

/*
	动态规划
	本问题可以分成多个子问题,爬第n阶楼梯的方法数量,等于 2 部分之和
    爬上 n-1 阶楼梯的方法数量。因为再爬1阶就能到第n阶
    爬上 n-2 阶楼梯的方法数量,因为再爬2阶就能到第n阶
    所以我们得到公式 F(n) = F(n-1) + F(n-2)
	同时需要初始化 F(1) = 1, F(2) = 2
	
	时间复杂度O(n)
	空间复杂度O(n)
	迭代 自底向上
*/

class Solution {
public:
    int climbStairs(int n) {
        if (n == 1) { return 1; }
        if (n == 2) { return 2; }
        vector<int> dp(n+1);	//0~n,有n+1个数
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++){
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }
};

解法3优化

/*
	对空间进行优化, 因为第n个结果只跟前两个状态有关,则只定义两个变量存储即可

	时间复杂度O(n)
	空间复杂度O(1)
*/
class Solution {
public:
    int climbStairs(int n) {
        if (n == 1) { return 1; }
        if (n == 2) { return 2; }
        int pre = 2, ppre = 1;
        int ret = 0;
        for (int i = 3; i <= n; i++){
            ret = pre + ppre;
            ppre = pre;
            pre = ret;
        }
        return ret;
    }
};

知识点

算法

动态规划, 排列组合,递归, 斐波那契

77.组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

输入:n = 4, k = 2
输出:
[
[1,2],[1,3],[1,4],
[2,3],[2,4],
[3,4],
]

解法1.回溯

class Solution {
public:
    vector<vector<int>> ret;
    vector<vector<int>> combine(int n, int k) {
        ret.clear();

        vector<int> path;
        backtrack(path, n, k, 1);
        return ret;
    }

    void backtrack(vector<int> &path, int n, int k, int start) {
        if (path.size() == k) {			//递归出口, ret的push时机
            ret.push_back(path);
            return;
        }
        //剪枝
        //...
        for (int i = start; i <= n; i++) {
            path.push_back(i);			//选择
            
            backtrack(path, n, k, i+1);	//递归
            path.pop_back();			//回溯, 撤销选择
        }
    }
};
例如 n=4, k=2
[    1,    2,    3,    4    ]
   | | |  | |    |     |
   2 3 4  3 4    4[1,2] [1,3] [1,4] | [2,3] [2,4] | [3,4]

方法和78极其相似.

剪枝需求: 以n=7, k=5为例
[3,4,5,6,7]√
[4,5,6,7] x
也就是超过了某个值开始,后面就没有必要再遍历了,这个start不难看出>n-k+1开始的都是没办法凑齐k个数的,可以剪枝

知识点

算法

回溯

78.子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 **互不相同

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

解法1.位运算

/*
	10 / 10 个通过测试用例
    状态:通过
    执行用时: 0 ms
    内存消耗: 7 MB
    
    时间复杂度O(n*2^n)一共 2^n个状态,每种状态需要 O(n)的时间来构造子集。
*/
class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> ret;
        vector<int> sub;
        for (int i = 0; i <= pow(2, nums.size())-1; i++) {
            sub.clear();
            int ending = i == 0 ? 0 : log(i) / log(2);
            for (int j = 0 ; j <= ending; j++) {
                if (i & (1 << j)) {
                    sub.push_back(nums[j]);
                }
            }
            ret.push_back(sub);
        }
        return ret;
    }
};

例如{1,2,3}

0/1 序列子集0/10/1 序列对应的二进制数
0000
00111
01022
0111, 23
10034
1011, 35
1102, 36
1111, 2, 37

解法2.回溯

①递归树, 看下图

vector<vector<int>> ret;

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums)
    {
        vector<int> path;
        ret.clear();
        backtrack(nums, path, 0);
        return ret;
    }

//nums为题目中的给的数组
//path为路径结果,要把每一条 path 加入结果集
    void backtrack(vector<int>& nums,vector<int>&path, int start)
    {
        ret.push_back(path);	//把每一条路径加入结果集
/*
    ②找结束条件
    此题非常特殊,所有路径都应该加入结果集,所以不存在结束条件。或者说当 start 参数越过数组边界的时候,
    程序就自己跳过下一层递归了,因此不需要手写结束条件,直接加入结果集
*/
/*
	③找选择列表
	子集问题的选择列表,是上一条选择路径之后的数
*/
        
        for(int i=start;i<nums.size();i++) {
/*
	④判断是否需要剪枝
	从递归树中看到,路径没有重复的,也没有不符合条件的,所以不需要剪枝 
*/
            path.push_back(nums[i]);		//⑤做出选择
            backtrack(nums,path,i+1);
            path.pop_back();				//⑥撤销选择
        }
    }
};

选择列表里的数,都是选择路径(红色框)后面的数,比如[1]这条路径,他后面的选择列表只有"2、3",[2]这条路径后面只有"3"这个选择,那么这个时候,就应该使用一个参数start,来标识当前的选择列表的起始位置。也就是标识每一层的状态,因此被形象的称为"状态变量"

解法2的另一种写法

vector<vector<int>> ret;

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<int> path;
        ret.clear();
        backtrack(nums, path, 0);
        return ret;
    }

    void backtrack(vector<int>& nums,vector<int>&path, int idx)
    {
        if (nums.size() == idx) {
            ret.push_back(path);
            return;
        }
        path.push_back(nums[idx]);	//选择
        backtrack(nums,path,idx+1);
        path.pop_back();			//撤销选择,再回溯
        backtrack(nums,path,idx+1);
    }
};

如果上诉的递归树画成另一种形式:
与上诉图的区别是思想上的区别, 上图是给出选择1,2,3 只能按顺序选剩下的, 例如一开始选择了2, 则下一次只能选2后面的3
出口为没得选了的时候
ret什么时候选择push_back子集? 每一次路径都push.

下图的思想是枚举 i : 0 ~ n-1,每次选择选i还是不选i,就有了下图
出口为idx == n,即下标溢出的时候
push_back操作放在出口处,此时的子集全部相加起来就是全集.

知识点

算法

二进制思想, 回溯

200.岛屿数量

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。

输入:grid = [
[“1”,“1”,“0”,“0”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“1”,“0”,“0”],
[“0”,“0”,“0”,“1”,“1”]
]
输出:3

解法1.dfs

class Solution {
public:
    int count = 0;
    int numIslands(vector<vector<char>>& grid) {
        for(int i = 0; i < grid.size(); i++) {
            for(int j = 0; j < grid[0].size(); j++) {
                if(grid[i][j] == '1') {
                    count++;
                    dfs(grid, i, j);
                }
            }
        }

        return count;
    }
    
    void dfs(vector<vector<char>>& grid, int r, int c) {
        //判断 base case
        //如果坐标(r,c)超出了网格范围,直接返回
        if(!isArea(grid, r, c)) return;
        
        //如果这个格子不是岛屿,直接返回
        if (grid[r][c] != '1') return;
        
        grid[r][c] = 2; //将格子标记为【已遍历过】
        
        //访问上、下、左、右四个相邻结点
        dfs(grid, r-1, c);
        dfs(grid, r+1, c); 
        dfs(grid, r, c-1); 
        dfs(grid, r, c+1);
    }

    //判断坐标(r,c)是否在网格中
    bool isArea(vector<vector<char>>& grid, int r, int c) {
        return (0 <= r && r < grid.size() && 0 <= c && c < grid[0].size());
    }
};

每次发现一座岛屿就把周围临近全感染了的感觉.

解法2.并查集

class Solution {
public:
    //并
    void merge(vector<int> &par, int index1, int index2) {
        if (find(par, index1) == find(par, index2)) {	//检查是不是同一个集合的,避免重复合并
            return;
        }
        par[find(par, index1)] = find(par, index2);		//index2成为(index1的祖先)的祖先
    }
    
    //查
    int find(vector<int> &par, int index) {
        if (par[index] != index) {
            par[index] = find(par, par[index]);	//压缩路径, index直接指向祖先, 下一次查询的时间复杂度就是O(1)
        }
        return par[index];
    }

    int getIndex(int x, int y) {
        if (y*width + x >= height*width) {
            //抛出异常
        }
        return y*width + x;
    }

    int numIslands(vector<vector<char>>& grid) {
        int count = 0;
        height = grid.size();
        width = grid[0].size();

        vector<int> par(width*height);
        
        //init
        for(int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (grid[y][x] == '1') {	//所有岛屿一开始祖先都是自己
                    par[getIndex(x, y)] = getIndex(x, y);
                }
            }
        }

        //merge
        for(int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (grid[y][x] == '1') {	//与周围连接的冰川合并
                    if (x+1 < width && grid[y][x+1] == '1')  merge(par, getIndex(x, y), getIndex(x+1, y));
                    if (y+1 < height && grid[y+1][x] == '1') merge(par, getIndex(x, y), getIndex(x, y+1));
                }
            }
        }

        //calc
        for(int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (grid[y][x] == '1' && par[getIndex(x, y)] == getIndex(x, y)) {	//寻找所有的岛屿指向的祖先(山峰)有多少, 就有多少岛屿
                    count++;
                }
            }
        }
        return count;
    }
private:
    int width;
    int height;
};

知识点

算法

dfs, 并查集

203.移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

解法1. 链表的基本操作

/*
    66 / 66 个通过测试用例
    状态:通过
    执行用时: 16 ms
    内存消耗: 14.5 MB
*/
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode *cur = head;
        ListNode *pre = NULL;

        while (cur) {
            if (cur->val == val) {	//前节点指向当前节点的后节点, 当前节点后移
                if (cur == head) {	//头结点特殊处理
                    head = cur->next;
                    cur = head;
                } else {
                    pre->next = cur->next;
                    cur = cur->next;
                }
            } else {				//前节点后移,当前节点也后移
                pre = cur;
                cur = cur->next;
            }
        }
        return head;
    }
};

解法1.别人的版本

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        struct ListNode* dummyHead = new ListNode(0, head);	//设立哨兵节点, 就不用像上述解法对头结点特殊处理
        struct ListNode* temp = dummyHead;	//用于遍历的节点
        while (temp->next != NULL) {
            if (temp->next->val == val) {
                temp->next = temp->next->next;	//判断下一个的值而不是当前的值的话就可以不用像我上述解法那样定义pre和cur两个遍历指针.
            } else {
                temp = temp->next;
            }
        }
        return dummyHead->next;	//返回哨兵节点的下一个
    }
};

解法2.递归

/*
	递归 总是这么的优美简短
	不过也过于抽象
*/
struct ListNode* removeElements(struct ListNode* head, int val) {
    if (head == NULL) {
        return head;
    }
    head->next = removeElements(head->next, val);
    return head->val == val ? head->next : head;
}

206.反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

解法1.双(三?)指针

/*
    28 / 28 个通过测试用例
    状态:通过
    执行用时: 4 ms
    内存消耗: 8.1 MB
*/
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = NULL;
        ListNode* cur = head;
        while (cur) {
            ListNode* after = cur->next;
            cur->next = pre;
            pre = cur;
            cur = after;
        }
        return pre;
    }
};

解法2.递归

/**
    * 以链表1->2->3->4->5举例
    * @param head
    * @return
*/
public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) {
        /*
                直到当前节点的下一个节点为空时返回当前节点
                由于5没有下一个节点了,所以此处返回节点5
             */
        return head;
    }
    //递归传入下一个节点,目的是为了到达最后一个节点
    ListNode newHead = reverseList(head.next);
    /*
            第一轮出栈,head为5,head.next为空,返回5
            第二轮出栈,head为4,head.next为5,执行head.next.next=head也就是5.next=4,
                      把当前节点的子节点的子节点指向当前节点
                      此时链表为1->2->3->4<->5,由于4与5互相指向,所以此处要断开4.next=null
                      此时链表为1->2->3->4<-5
                      返回节点5
            第三轮出栈,head为3,head.next为4,执行head.next.next=head也就是4.next=3,
                      此时链表为1->2->3<->4<-5,由于3与4互相指向,所以此处要断开3.next=null
                      此时链表为1->2->3<-4<-5
                      返回节点5
            第四轮出栈,head为2,head.next为3,执行head.next.next=head也就是3.next=2,
                      此时链表为1->2<->3<-4<-5,由于2与3互相指向,所以此处要断开2.next=null
                      此时链表为1->2<-3<-4<-5
                      返回节点5
            第五轮出栈,head为1,head.next为2,执行head.next.next=head也就是2.next=1,
                      此时链表为1<->2<-3<-4<-5,由于1与2互相指向,所以此处要断开1.next=null
                      此时链表为1<-2<-3<-4<-5
                      返回节点5
            出栈完成,最终头节点5->4->3->2->1
         */
    head.next.next = head;
    head.next = null;
    return newHead;
}

209.长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

解法1.滑动窗口

/*
		4 ms	10.1 MB
		先绘画好一个符合条件的窗口,然后不断右移寻求最优解
*/

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int curLen = 0, curSum = 0;
        for (int i = 0; curSum < target; i++) {	//绘画初始窗口
            if (i >= nums.size()) {		//全加起来都不满足, 直接返回0
                return 0;
            }
            curSum += nums[i];
            curLen++;
        }
        int minLen = curLen;
        for (int i = 1; i < nums.size(); i++) { //i表示偏移单位
            curSum -= nums[i-1];
            curLen--;                           //左边界右移
            while (curSum < target && i+curLen < nums.size()) { //右边界移到比目标大或者尽头了为止
                curSum += nums[i+curLen];   //i+curLen代表窗口右边一个元素
                curLen++;
            }
            if (curSum >= target) {             //符合条件,记录对比下
                minLen = min(curLen, minLen);
            }
        }
        return minLen;
    }
};

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

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

解法1.库函数法!

/*
	0 ms	9.6 MB
*/
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        sort(nums.begin(), nums.end(), greater<int>());
        return nums[k-1];
    }
};

解法2.自己实现排序

 
        }
        int mid = partition(nums, left, right);	//基准轴划分出两个区域
        quickSort(nums, left, mid-1);			//左边区域都是比基准值小的
        quickSort(nums, mid+1, right);			//右边区域都是比基准值大的
    }

    //把[left,right]划分两区域
    int partition(vector<int>& nums, int left, int right) {
        //选取nums[left]为基准值.|left| ~ i ~ j ~ right|
        //第一区域是[left, left]基准值区, 第二区域是[left,i)比基准值小的区, 第三区域[i,j)为比基准值大的区, 第四区域[j,right]为未探索区
        int i = left+1, j = left+1;
        for (; j < nums.size(); j++) {
            if (nums[j] < nums[left]) {	//当j遇到比基准值小的数, j就和i交换,这样就能保证第二区域增加一个元素, 第四区域减少一个元素
                swap(nums[i], nums[j]);
                i++;/*
	库函数排序:						0 ms	9.6 MB
	k < 200 选择排序, else 归并排序:  32 ms	36.7 MB
	归并排序:						85 ms	39.9 MB
	topK版选择排序: 					172 ms	9.6 MB
	插入排序:						450 ms	9.8 MB
	完整排序版选择排序:				532 ms	9.8 MB
	快速排序:						648 ms	9.7 MB	??????????
	冒泡排序:						832 ms	9.6 MB
	
*/
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        if (k <= 100) {	//k比较小则采用选择排序,只排k次
            testSort(nums, SELECT);
        } else {		//否则用归并排序
            testSort(nums, MERGE);
        }
        print(nums);
        return nums[nums.size()-k];
    }

private:
    typedef enum _SORT_TYPE_ {
        LIBRARY = 0,
        BUBBLE,
        SELECT,
        INSERT,
        QUICK,
        MERGE,
        // HEAP,
    }sortType;

    void testSort(vector<int>& nums, sortType type) {
        switch (type) {
            case LIBRARY:
                sort(nums.begin(), nums.end());
                break;
            case BUBBLE:
                bubbleSort(nums);
                break;
            case SELECT:
                selectSort(nums);
                break;
            case INSERT:
                insertSort(nums);
                break;
            case QUICK:
                quickSort(nums);
                break;
            case MERGE:
                mergeSort(nums);
                break;
        }
    }

    //冒泡排序实现
    void bubbleSort(vector<int>& nums) {
        for (int i = 0; i < nums.size() - 1; i++) {
            bool ok = true;		//如果有一轮完全没有交换过元素,说明已经不需要再排了
            for (int j = 0; j < nums.size() - 1 - i; j++) {
                if (nums[j] > nums[j+1]) {
                    swap(nums[j], nums[j+1]);
                    ok = false;
                }
            }
            if (ok) return;
        }
    }

    //选择排序实现,可选择只排k次
    void selectSort(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0; i < n; i++) {
            int max = 0;
            for (int j = 0; j < n - i; j++) {
                max = nums[j] > nums[max] ? j : max;
            }
            swap(nums[max], nums[n-1-i]);	//比起冒泡排序,就好在一轮只需要交换一次元素
            if (i>=topK-1) {
                break;
            }
        }
    }
    
    //插入排序实现
    void insertSort(vector<int>& nums) {
        int n = nums.size();
        for (int i = 1; i < n; i++) {
            int val = nums[i];
            int j = i;
            while (j > 0 && nums[j-1] > val) {
                nums[j] = nums[j-1];
                j--;
            }
            nums[j] = val;
        }
    }

   //快速排序start========================
    void quickSort(vector<int>& nums) {
        quickSort(nums, 0, nums.size()-1);
    }

    void quickSort(vector<int>& nums, int left, int right) {
        if (left >= right) {	//递归出口, 左右指针一致
            return;
        }
        int mid = partition(nums, left, right);	//基准轴划分出两个区域
        quickSort(nums, left, mid-1);			//左边区域都是比基准值小的
        quickSort(nums, mid+1, right);			//右边区域都是比基准值大的
    }

    //把[left,right]划分两区域
    int partition(vector<int>& nums, int left, int right) {
        //选取nums[left]为基准值.|left| ~ i ~ j ~ right|
        //第一区域是[left, left]基准值区, 第二区域是[left,i)比基准值小的区, 第三区域[i,j)为比基准值大的区, 第四区域[j,right]为未探索区
        int i = left+1, j = left+1;
        for (; j < nums.size(); j++) {
            if (nums[j] < nums[left]) {	//当j遇到比基准值小的数, j就和i交换,这样就能保证第二区域增加一个元素, 第四区域减少一个元素
                swap(nums[i], nums[j]);
                i++;
            }
        }
        swap(nums[i-1], nums[left]);	//因为i是第三区域的开始,所以i-1刚好是最后一个比基准值小的数, 那这个数和基准值交换位置,刚好为中间
        return i-1;
    }
    //快速排序start========================

    //归并排序start========================
    void mergeSort(vector<int>& nums) {
        mergeSort(nums, 0, nums.size());
    }

    void mergeSort(vector<int>& nums, int left, int right) {
        if (right - left <= 1) {        //递归出口,单元素
            return;
        }
        int mid = (left+right) / 2;		//从中间划分
        mergeSort(nums, left, mid);     //拆分左边,并序列化
        mergeSort(nums, mid, right);    //拆分右边,并序列化
        merge(nums, left, mid, right);  //合并两个有序序列
    }

    //合并两个有序序列: nums[l,m) & nums[m,r)
    void merge(vector<int>& nums, int left, int mid, int right){
        vector<int> copyLeft(nums.begin()+left, nums.begin()+mid);	//防止破坏原数组
        vector<int> copyRight(nums.begin()+mid, nums.begin()+right);
        int i = 0, j = 0;
        int k = left;
        copyLeft.push_back((unsigned int)(~0) >> 1);	//相当于正无穷, 哨兵元素
        copyRight.push_back((unsigned int)(~0) >> 1);
        while (k < right) {	//接下来就是非常常规的合并两个有序序列操作了,两两比较, 谁小拿谁
            if (copyLeft[i] < copyRight[j]) {
                nums[k] = copyLeft[i];
                i++;
            } else {
                nums[k] = copyRight[j];
                j++;
            }
            k++;
        }
    }
    //归并排序end==========================

    void swap(int& n1, int& n2) {
        if (n1 == n2) {
            return;
        }
        int t = n1;
        n1 = n2;
        n2 = t;
    }

    void print(vector<int>& nums) {
        cout << endl;
        for (int i = 0; i < nums.size(); i++) {
            cout << nums[i] << ", ";
        }
        cout << endl;
    }
};

217.存在重复元素

给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false

解法1.set

class Solution {
public:
    bool containsDuplicate(vector<int>& nums) {
        unordered_set<int> table;
        for (int i = 0; i < nums.size(); i++) {
            if (!table.insert(nums[i]).second) {    //插入失败代表set元素重复, 所以返回true
                return true;
            }
        }
        return false;
    }
};

225. 用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

解法1.常规

/*
	双队列实现
	时间复杂度O(n),因为插入一个main要全部依次移动至sub
*/
class MyStack {
public:
    MyStack() {

    }
    
    void push(int x) {
        sub.push(x);
        while(!empty()) {
            sub.push(pop());
        }
        swap(main, sub);
    }
    
    int pop() {
        int val = top();
        main.pop();
        return val;
    }
    
    int top() {
        return main.front();
    }
    
    bool empty() {
        return main.empty();
    }

private:
    queue<int> main;
    queue<int> sub;
};

例子模拟说明: A B C 依次入队

  1. A入队sub, main与sub交换 // main: |A|
  2. B入队sub, A从main出队, 进入sub main与sub交换 //main: |A B|
  3. C入队sub, B从main出队, 进入sub //main: [A], sub: [B C]
  4. A从main出队, 进入sub, main与sub交换 // main: [A B C]
/*
	单队列实现
	时间复杂度也是O(n)
*/
class MyStack {
public:
    MyStack() {

    }
    
    void push(int x) {	//先入队, 将原队列的出队再入一次
        int n = qu.size();
        qu.push(x);
        while(n > 0) {
            qu.push(pop());
            n--;
        }
    }
    
    int pop() {
        int val = top();
        qu.pop();
        return val;
    }
    
    int top() {
        return qu.front();
    }
    
    bool empty() {
        return qu.empty();
    }

private:
    queue<int>  qu;
};

235.用栈实现队列

解法1.常规

/*
	时间复杂度:
		push: 因为每次push都要循环两次[2n],那push n个数, 就是O(n^2)
		pop: O(1)
		peek: O(1)
*/
class MyQueue {
public:
    MyQueue() {

    }
    
    void push(int x) {	//每次push时间复杂度都是O(2n), 低效
        while (!main.empty()) {
            int val = main.top();
            main.pop();
            sub.push(val);
        }
        sub.push(x);
        while (!sub.empty()) {
            int val = sub.top();
            sub.pop();
            main.push(val);
        }
    }
    
    int pop() {
        int val = main.top();
        main.pop();
        return val;
    }
    
    int peek() {
        return main.top();
    }
    
    bool empty() {
        return main.empty();
    }

private:
    stack<int> main; 
    stack<int> sub; 
};
  1. push A: main中无元素, A入栈sub,然后再出栈sub, 再入栈main //|A| 这边是底
  2. push B: main中A出栈, 入栈到sub, 然后B也压栈到sub, 然后B, A依次出栈到main //|A B|
  3. push C: 同样, A, B依次出栈, 入栈到sub |B A|, 然后C也入栈 |C B A|, 然后再依次出栈到main //|A B C|
  4. pop: 直接对main进行pop操作即可 //|B C|

解法1.优化

/*
	官方
	每次pop或peek 时,若输出栈为空则将输入栈的全部数据依次弹出并压入输出栈,这样输出栈从栈顶往栈底的顺序就是队列从队首往队尾的顺序。
	时间复杂度:
		push: O(1)
		pop: push了n个数之后, pop一次要进行一次全出栈入栈 O(n), 可是之后再取n-1次, 都是O(1)了
		peek: 与pop共享时间复杂度
*/
class MyQueue {
private:
    stack<int> inStack, outStack;

    void in2out() {
        while (!inStack.empty()) {
            outStack.push(inStack.top());
            inStack.pop();
        }
    }

public:
    MyQueue() {}

    void push(int x) {	//push不需要处理
        inStack.push(x);
    }

    int pop() {	//pop如果当前输出栈为空, 会将之前在输入栈的全部压去输出栈,那下次pop就是O(1)的时间复杂度
        if (outStack.empty()) {
            in2out();
        }
        int x = outStack.top();
        outStack.pop();
        return x;
    }

    int peek() {
        if (outStack.empty()) {
            in2out();
        }
        return outStack.top();
    }

    bool empty() {
        return inStack.empty() && outStack.empty();
    }
};
  1. push A, B, C 正常对输入栈进行push操作 in: |C B A|
  2. pop: 将输入栈所有元素依次出栈到输出栈 out: |A B C| 然后就可以正常的对输出栈进行pop操作了
  3. pop: 再pop一次, 因为此时输出栈不为空, 直接正常pop即可.
  4. peek: 取队列头一样, 输出栈不为空就直接取栈顶就是相当于队列头了

322.零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。

输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

解法1.动态规划

/*
	120 ms	14.1 MB
	自己写的dp
*/
#define MIN(x, y) ((x) < (y) ? (x) : (y))

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if (amount == 0) {
            return 0;
        }
        int mini = -1;
        vector<int> dp(amount+1, -1);
        for (auto it = coins.begin(); it != coins.end();it++) {
            if (*it > amount) {
                coins.erase(it);    //删除元素后,迭代器会指向下一个元素,此时做it--操作回退
                it--;
                continue;
            }
            mini = (mini == -1) ? *it : MIN(mini, *it);
            dp[*it] = 1;
        }
        if (amount < mini) {
            return -1;
        }
        for (int i = 1; i <= amount; i++) { //其实计算了很多多余的dp
            if (dp[i] == -1) {
                for (int coin = 0; coin < coins.size(); coin++) {
                    int next = i - coins[coin];
                    if (next > 0 && dp[next] != -1) {
                        dp[i] = dp[i] == -1 ? (1 + dp[next]) : MIN(dp[i], 1 + dp[next]);
                    }
                }
            }
        }
        return dp[amount];
    }
};
/*
	84 ms	14.1 MB
	官方dp
	看起来和我写的思路和剪枝没啥很大区别, 不过简洁点
*/

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int Max = amount + 1;
        vector<int> dp(amount + 1, Max);
        dp[0] = 0;
        for (int i = 1; i <= amount; ++i) {
            for (int j = 0; j < (int)coins.size(); ++j) {
                if (coins[j] <= i) {
                    dp[i] = min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
};

解法2.dfs

/*
	麻了, 超时了,可能剪枝不够彻底
	应该是要进行记忆化搜索, 可是这不知道怎么改
*/
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        minCount = -1;
        dfs(coins, amount, 0);
        return minCount;
    }


    void dfs(vector<int>& coins, int amount, int count) {
        if (amount <= 0) {
            if (amount == 0) {
                minCount = minCount == -1 ? count : (count < minCount ? count : minCount); 
            }
           return;
        }
        for (int i = 0; i < coins.size(); i++) {
            dfs(coins, amount - coins[i], count+1);
        }
    }

private:
    int minCount;
};

374.猜数字大小

猜数字游戏的规则如下:

每轮游戏,我都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。
你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):

-1:我选出的数字比你猜的数字小 pick < num
1:我选出的数字比你猜的数字大 pick > num
0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num
返回我选出的数字。

解法1.二分查找

/** 
 * Forward declaration of guess API.
 * @param  num   your guess
 * @return 	     -1 if num is lower than the guess number
 *			      1 if num is higher than the guess number
 *               otherwise return 0
 * int guess(int num);
 */

class Solution {
public:
    int guessNumber(int n) {
        int l = 0;		//左边界0开始
        int r = n-1;	//右边界n-1开始

        while (l <= r) {	//左边界在右边界的右边, 跳出循环
            int mid = ((r-l) >> 1) + l;	//看附件
            int ret = guess(mid);		//猜
            if (ret == 0) {				//猜中了
                return mid;
            } else if (ret < 0) {		//猜的比中位数数小, 右边界向左移动
                r = mid - 1;
            } else {    				//ret > 0, 猜的比中位数大, 左边界向右移动
                l = mid + 1;
            }
        }
        return l;
    }
};

究极经典的二分查找

389.找不同

给定两个字符串 st ,它们只包含小写字母。
字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。
请找出在 t 中被添加的字母。

解法1.map常规

/*
    执行用时: 8 ms
    内存消耗: 6.5 MB
*/
class Solution {
public:

    bool isValid(char ch) {
        map<char, int>::iterator it = tbl.find(ch);
        if (it != tbl.end() && it->second > 0) {
            return true;
        }
        return false;
    }

    char findTheDifference(string s, string t) {
        tbl.clear();
        for (int i = 0; i < s.length(); i++) {  //initialize
            if (isValid(s[i])) {
                tbl[s[i]] = tbl[s[i]] + 1;
            } else {
                tbl.insert(pair<char, int>(s[i], 1));
            }
        }


        for (int i = 0; i <= t.length(); i++) {
            if (!isValid(t[i])) {
                return t[i];
            }
            tbl[t[i]] = tbl[t[i]] - 1;
        }
        return t[0];
    }
private:
    map<char, int> tbl;
};

解法2.巧用ascii

/*
    执行用时: 4 ms
    内存消耗: 6.4 MB
*/
//利用两ascii和之差即为多出来的字母对应的ascii,转型返回即可
class Solution {
public:
    char findTheDifference(string s, string t) {
        int ascii = 0;
        for (int i = 0; i <= t.length(); i++) {
            ascii += t[i];
        }

        for (int i = 0; i < s.length(); i++) {
            ascii -= s[i];
        }
        return static_cast<char>(ascii);
    }
};


/*
	0 ms	6.4 MB
	优化到极致, 减少一次n的循环
*/
class Solution {
public:
    char findTheDifference(string s, string t) {
        int ascii = t[0];
        for (int i = 0; i <= s.length(); i++) {
            ascii = ascii + t[i+1] - s[i];
        }
        return static_cast<char>(ascii);
    }
};

547.省份数量

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。

例如

isConnected = [
	[1,1,0,0],
	[1,1,0,0],
	[0,0,1,1],
	[0,0,1,1],
]

则代表下图所示:

由于isConnecter[i][j] == isConnecter[j][i], 且isConnecter[i][i] = true, 所以只看0-n对角线的上半部分即可,可以看出来相连情况为[0 1]和[2 3], 所以结果应该是2.

解法1.dfs

/*
	按照自己思路写的版本
	时间复杂度不会算
	
	113 / 113 个通过测试用例
    状态:通过
    执行用时: 20 ms
    内存消耗: 13.7 MB
*/
class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        int count = 0;
        set<int> visited;							//记录下已经访问过的城市,访问过的城市不再便利,因为它已经属于某一个省份了
        for (int i = 0; i < isConnected.size(); i++) {	//两个for构成右上半的三角范围
            for (int j = 0; j <= i; j++) {
                if (isConnected[i][j] == 1 && visited.count(i) == 0 && visited.count(j) == 0) {
                    count++;
                    dfs(isConnected, visited, i, j);
                }
            }
        }
        return count;
    }

    void dfs(vector<vector<int>>& isConnected, set<int> &visited, int x, int y) {
        int n = isConnected.size();
        //xy不相连则返回, 因为这次的x是递归进来的y, 所以肯定是在visited里的, 所以不能包含visited.count(x) != 0的条件
        if (isConnected[x][y] != 1  || visited.count(y) != 0) {
            return;
        }
        visited.insert(y);  //还是因为这次的x是递归进来的y, 所以上个递归已经把x给放进visited了,不需要再写
        for (int i = 0; i < n; i++) {
            dfs(isConnected, visited, y, i);
        }
    }
};

解法2.并查集

/*
    113 / 113 个通过测试用例
    状态:通过
    执行用时: 16 ms
    内存消耗: 13.3 MB
*/
class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
       int center = 0;
       int num = isConnected.size();
       vector<int> parent(num);

        //init
        for (int i = 0; i < num; i++) {
            parent[i] = i;  //未合并前, 城市i的所在省的省会就是i
        }

        //合并连接的两个城市
        for (int i = 0; i < num; i++) {
            for (int j = 0; j < i; j++) {
                if (isConnected[i][j] == 1) {
                    merge(parent, i, j);
                }
            }
        }

        //有多少个省会就是多少个省
        for (int i = 0; i < num; i++) {
            if (parent[i] == i) {
                center++;
            }
        }
        return center;
    }

    //并
    void merge(vector<int> &parent, int index1, int index2) {
        //合并, index1的所属省成为index2所属省的一部分
        parent[find(parent, index1)] = find(parent, parent[index2]);
    }
    
    //查
    int find(vector<int> &parent, int idx) {    //idx为城市代号, 寻找indx的祖宗节点(省会)
        if (parent[idx] != idx) {				//index的父节点不是自己,说明不是省会
            parent[idx] = find(parent, parent[idx]);	//那就再找, 并且压缩路径, 即城市idx直接指向省会
        }
        return parent[idx];
    }
};

知识点

算法

dfs, 并查集

622.设计循环队列

设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。

解法1.常规

/*
    58 / 58 个通过测试用例
    状态:通过
    执行用时: 20 ms
    内存消耗: 16.3 MB
    
	几年前看过循环队列的设计,没想到还能想起来,针不错
*/

class MyCircularQueue {
public:
    MyCircularQueue(int k) {
        m_que.resize(k);
        m_maxLen = k;
        m_start = 0;
        m_len = 0;
    }

    int getValue(int idx) {
        return m_que[realIndex(idx)];
    }

    void setValue(int idx, int val) {
        m_que[realIndex(idx)] = val;
    }

    bool enQueue(int value) {
        if (isFull()) {
            return false;
        }
        m_start = m_len == 0 ? 0 : realIndex(m_start-1);    //队头让给新元素
        setValue(m_start, value);
        m_len++;
        return true;
    }
    
    bool deQueue() {
        if (isEmpty()) {
            return false;
        }
        m_len--;    //不需要将此元素初始化,长度减就代表出队了
        return true;
    }
    
    int Front() {
        return isEmpty() ? -1 : getValue(m_start+m_len-1);  //出队的是头
    }
    
    int Rear() {
        return isEmpty() ? -1 : getValue(m_start);          //入队的地方是尾
    }
    
    bool isEmpty() {
        return m_len <= 0;
    }
    
    bool isFull() {
        return m_len >= m_maxLen;
    }

private:
    int m_maxLen;
    int m_start;
    int m_len;
    vector<int> m_que;
    
    int realIndex(int idx) {    //获取正确的下标, 防下标越界
        return (idx+m_maxLen) % m_maxLen;
    }
};

705.设计哈希集合

不使用任何内建的哈希表库设计一个哈希集合(HashSet)。

解法1.链地址法

/*
	官方解法之链地址法
*/
class MyHashSet {
private:
    vector<list<int>> data;
    static const int base = 769;
    static int hash(int key) {
        return key % base;
    }
public:
    /** Initialize your data structure here. */
    MyHashSet(): data(base) {}
    
    void add(int key) {
        int h = hash(key);
        for (auto it = data[h].begin(); it != data[h].end(); it++) {
            if ((*it) == key) {
                return;
            }
        }
        data[h].push_back(key);
    }
    
    void remove(int key) {
        int h = hash(key);
        for (auto it = data[h].begin(); it != data[h].end(); it++) {
            if ((*it) == key) {
                data[h].erase(it);
                return;
            }
        }
    }
    
    /** Returns true if this set contains the specified element */
    bool contains(int key) {
        int h = hash(key);
        for (auto it = data[h].begin(); it != data[h].end(); it++) {
            if ((*it) == key) {
                return true;
            }
        }
        return false;
    }
};

哈希函数:能够将集合中任意可能的元素映射到一个固定范围的整数值,并将该元素存储到整数值对应的地址上。
冲突处理:由于不同元素可能映射到相同的整数值,因此需要在整数值出现「冲突」时,需要进行冲突处理。总的来说,有以下几种策略解决冲突:

  • 开放地址法:当发现哈希值 hh 处产生冲突时,根据某种策略,从 hh 出发找到下一个不冲突的位置。例如,一种最简单的策略是,不断地检查 h+1,h+2,h+3,\ldotsh+1,h+2,h+3,… 这些整数对应的位置。
  • 再哈希法:当发现哈希冲突后,使用另一个哈希函数产生一个新的地址。
  • 链地址法:为每个哈希值维护一个链表,并将具有相同哈希值的元素都放入这一链表当中。

扩容:当哈希表元素过多时,冲突的概率将越来越大,而在哈希表中查询一个元素的效率也会越来越低。因此,需要开辟一块更大的空间,来缓解哈希表中发生的冲突。

1456.定长子串中元音最大数目

给你字符串 s 和整数 k 。
请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。
英文中的 元音字母 为(a, e, i, o, u)。

输入:s = “leetcode”, k = 3
输出:2
解释:“lee”、“eet” 和 “ode” 都包含 2 个元音字母。

解法1.滑动窗口

/*
    106 / 106 个通过测试用例
    状态:通过
    执行用时: 16 ms
    内存消耗: 9.8 MB
*/
class Solution {
public:
    bool isYuanYin(char ch) {
        switch (ch) {
            case 'a':
            case 'e':
            case 'i':
            case 'o':
            case 'u':
                return true;
        }
        return false;
    }

    int maxVowels(string s, int k) {
        int n = s.size();
        int maxCnt = 0;
        for (int i = 0; i < k; i++){ //绘画一个基础窗口,长度为k
            maxCnt = isYuanYin(s[i]) ? (maxCnt + 1) : maxCnt;
        }

        int curCnt = maxCnt;
        for (int i = 1; i <= s.length() - k; i++) {
            if (isYuanYin(s[k-1+i])) {	//窗口右边界右移一个
                curCnt++;	//获得一个元音,cnt++
            }
            if (isYuanYin(s[i-1])) {	//窗口左边界也右移一个
                curCnt--;	//失去一个元音, cnt--
            }
            maxCnt = max(maxCnt, curCnt);	//记录最大窗口内元音个数
        }
        return maxCnt;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值