经典算法题 (持续补充中)

文章目录

  • 一. 链表
    • 1. 合并K个有序链表
    • 2. 翻转指定区间的链表
  • 二. 数组
    • 1. 和为k的连续子数组的最大长度
    • 2. 缺失的第一个正数
    • 3. 股票的最大利润
  • 三. 二叉树
    • 1. 二叉树的最大路径和
    • 2. 在二叉树中找到两个节点的最近公共祖先
    • 3. 二叉树的右视图
  • 四. 栈和队列
    • 1. LRU
    • 2. 有效的括号
    • 3. 后缀表达式
    • 4. 表达式求值
  • 五. 字符串
    • 1. 判断IP地址是否合法
    • 2. 字符串出现次数的TopK问题
    • 3. 最长不包含重复字符子串
    • 4. 字符串最长公共前缀
    • 5. 字符串解码
    • 6. 文本左右对齐
  • 六. 动态规划
    • 1. 最长递增子序列
    • 2. 最长回文子串
    • 3. 01背包
    • 4. 子数组和的差最小
    • 5. 零钱兑换(完全背包问题)
  • 七. DFS/BFS
    • 1. 组合总和II
    • 2. 走迷宫
    • 3. 全排列
    • 4. 岛屿数量
  • 八. 数学
    • 1. 不用循环和条件语句求 n!
    • 2. 判断一个数是否为回文数
    • 3. 判断是否为3的幂
    • 4. 购买钉子
  • 九. 二分/双指针/位运算
    • 1. 寻找递增递减序列的峰值
    • 33. 接雨水
    • 31.
    • 31.
    • 31.
    • 31.
    • 31.
    • 31.


一. 链表


1. 合并K个有序链表

  • 题目描述

原题链接

  • 代码

法一:分治

class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {

        if(lists.empty()) return nullptr;

        while(lists.size() > 1) {

            lists.push_back(mergeTwoLists(lists[0], lists[1]));
            lists.erase(lists.begin());
            lists.erase(lists.begin());
        }

        return lists[0];
    }

    ListNode* mergeTwoLists(ListNode *l1, ListNode *l2) {

        auto dummy = new ListNode(-1);
        auto cur = dummy;
        
        while(l1 && l2) {
            if(l1->val < l2->val) {
            
                cur->next = l1;
                l1 = l1->next;
            }
            else {
            
                cur->next = l2;
                l2 = l2->next;
            }
            cur = cur->next;
        }

        cur->next = l1 ? l1 : l2;
        return dummy->next;
    }
};


2. 翻转指定区间的链表

  • 题目描述

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
原题链接

  • 代码

法一:prev和cur向前移动

此题类似传统的翻转整个链表问题,翻转指定区间的话需要确定好翻转的起始位置也就是翻转点的前一个节点,因为有可能从头开始翻转所以需要pre指向dummy.

class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {

        auto dummy = new ListNode(-1);
        ListNode *pre = dummy;
        dummy->next = head;
        for(int i = 0; i < left - 1; i ++)
            pre = pre->next;
        ListNode *prev = nullptr;
        ListNode *cur = pre->next;
        auto flag = cur;
        int cnt = right - left + 1;
        while(cnt --) {
            auto t = cur->next;
            cur->next = prev;
            prev = cur;
            cur = t;
        }
        pre->next = prev;
        flag->next = cur;
        return dummy->next;
    }
};

法二:固定cur不动,将后面的依次点头插

这是一种简洁巧妙的做法。

class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int m, int n) {
        ListNode* dummy=new ListNode(-1);
        ListNode* pre=dummy;
        dummy->next=head;
        
        for(int i=0;i<m-1;i++)
            pre=pre->next;
        ListNode* cur=pre->next;
        for(int i=m;i<n;i++){
            ListNode* t=cur->next;
            cur->next=t->next;
            t->next=pre->next;
            pre->next=t;
        }
        return dummy->next;
    }
};





二. 数组


1. 和为k的连续子数组的最大长度

  • 题目描述

如题,此题在leetcode上是付费题目

  • 代码

此题原理是:扫描数组的过程中利用前缀和 + 哈希搜索所有和为k的连续子数组,扫描过程中记录最大长度;

值得注意的一点是 mp[0] = -1 这个边界条件,其表示:前缀和为0的子数组在最开始就出现了,对应的下标为-1。当然这个-1可能不会那么轻易看出来,测试一组用例即可得出-1。

核心原理大致是这样:
经典算法题 (持续补充中)_第1张图片

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        if(nums.empty())
            return 0;
        map<int, int> mp;
        mp[0] = -1;
        int sum = 0;
        int res =0;
        for(int i = 0;i<nums.size()-1; i++)
        {
            sum += nums[i];
            if(mp.count(sum-k))
                res = max(res, i - mp[sum - k]); 
            if(!mp.count(sum))
                mp[sum] = i;
        }
        return res;
    }
};


2. 缺失的第一个正数

  • 题目描述

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
原题链接

  • 代码

思路一:排序,去重,去非正整数

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {

        sort(nums.begin(), nums.end());
        nums.erase(unique(nums.begin(), nums.end()), nums.end());
        if(nums.back() <= 0) return 1;

        int k = 0;
        while(nums[k] <= 0) k ++;
        for(int i = 1, j = k; j < nums.size(); i ++, j ++) {
            if(nums[j] != i) 
                return i;
        }
        return nums.back() + 1;
    }
};


思路二:二分答案法

此题是我在面试网易的时候遇到的,其实第一种做法已经能做到O(n)了,面试官提示我用二分做。。。可惜最后我也没想到竟然用这种二分答案的方式,但是二分的做法并没有达到O(n)的复杂度,而是On(logn)。

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        
        //[1, max + 1]
        //为什么这道题可以用二分,主要看这个缺失的正整数x是否能将序列分为两部分:
        //原序列排好序且去重后,在序列中x左半边正整数个数=x-1个
        sort(nums.begin(), nums.end());
        nums.erase(unique(nums.begin(), nums.end()), nums.end()); //去重

        int max = nums.back() + 1;
        int l = 1, r = max;
        while(l < r) {
            int cnt = 0;
            int mid = l + r >> 1;
            for(auto num : nums){
                if(num > 0 && num < mid)
                    cnt ++;   //小于mid不重复的正整数
            }
            if(cnt < mid - 1) //答案在左侧
                r = mid - 1;
            else
                l = mid;      
        }
        return l;
    }
};



3. 股票的最大利润

  • 题目描述

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
原题链接

  • 代码
class Solution {
public:
    int maxProfit(vector<int>& prices) {

        int minPrice = INT_MAX;
        int maxProfit = 0;  // 如果所有的交易都小于0那么就不交易,就直接返回0
        for(auto x : prices) {
            if(x >= minPrice) {
                maxProfit = max(maxProfit, x - minPrice);
            } else {
                minPrice = x;
            }
        }
        return maxProfit;
    }
};





三. 二叉树


1. 二叉树的最大路径和

  • 题目描述

给定一个二叉树,请计算节点值之和最大的路径的节点值之和是多少。
这个路径的开始节点和结束节点可以是二叉树中的任意节点
原题链接

  • 代码

递归函数的意义是求出经过root的单边分支最大路径和。

class Solution {
public:
    int res = INT_MIN;
    int maxPathSum(TreeNode* root) {
        // write code here
        dfs(root);
        return res;
    }
    
    int dfs(TreeNode *root) {
        
        if(!root) return 0;
        
        int left_max = max(0, dfs(root->left));
        int right_max = max(0, dfs(root->right));
        int path = root->val + left_max + right_max;
        res = max(res, path);
        return root->val + max(left_max, right_max);
    }
};



2. 在二叉树中找到两个节点的最近公共祖先

  • 题目描述

给定一棵二叉树(保证非空)以及这棵树上的两个节点对应的val值 o1 和 o2,请找到 o1 和 o2 的最近公共祖先节点。
注:本题保证二叉树中每个节点的val值均不相同。
原题链接

  • 代码

非递归做法,哈希表存储父节点,记录路径。

class Solution {
public:
    int lowestCommonAncestor(TreeNode* root, int o1, int o2) {
        // write code here
        unordered_map<int, int> hash;
        queue<TreeNode*> q;
        q.push(root);
        hash.insert(make_pair(root->val, INT_MAX));
        while(q.size()) {
            if(hash.count(o1) && hash.count(o2)) //层序遍历提前终止,遍历到o1、o2即可。
                break;
            auto t = q.front();
            auto left = t->left;
            auto right = t->right;
            q.pop();
            if(left) {
                hash.insert(make_pair(left->val, t->val));
                q.push(left);
            }
            if(right) {
                hash.insert(make_pair(right->val, t->val));
                q.push(right);
            }
        }
        unordered_set<int> path;
        while(hash.count(o1)) {
            path.insert(o1);
            o1 = hash[o1];
        }
        while(!path.count(o2)) {
            o2 = hash[o2];
        }
        return o2;
    }
};



3. 二叉树的右视图

  • 题目描述

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
原题链接

  • 代码

这题和分层打印二叉树的解法类似。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        
        if(!root) return vector<int>();
        int l = depth(root);
        vector<vector<int>> res(l, vector<int>());
        queue<pair<int, TreeNode*>> q;
        q.push(make_pair(0, root));
        while(q.size()) {
            auto t = q.front();
            q.pop();
            res[t.first].push_back(t.second->val);
            if(t.second->left) {
                q.push(make_pair(t.first + 1, t.second->left));
            }
            if(t.second->right) {
                q.push(make_pair(t.first + 1, t.second->right));
            }
        }
        vector<int> ans;
        for(auto x : res) {
            ans.push_back(x.back());
        }
        return ans;
    }

    int depth(TreeNode *root) {
        if(!root) return 0;
        return max(depth(root->left), depth(root->right)) + 1;
    }
};





四. 栈和队列


1. LRU

  • 题目描述

原题链接

  • 代码
class Solution {
public:
    
    typedef std::pair<int, int> _kv;
    typedef std::list<_kv>::iterator _it_list;
    
    vector<int> LRU(vector<vector<int> >& operators, int k) {
        // write code here
        _cap = k;
        vector<int> res;
        for(auto &input : operators) {
            
            if(input[0] == 1)
                set(input[1], input[2]);
            else
                res.push_back(get(input[1]));
        }
        return res;
    }
    
    int remove(_it_list &it) {
        
        int key = it->first;
        int val = it->second;
        _L.erase(it);
        _H.erase(key);
        return val;
    }
    
    void add(int key, int val) {
        
        _L.push_front(make_pair(key, val));
        _H[key] = _L.begin();
        if(_L.size() > _cap) { //超出Cache,移除优先级最低的
            auto last = _L.end();
            remove(--last); 
        }
    }
    
    void set(int key, int val) {
        
        auto it = _H.find(key);
        if(it != _H.end()) {
            remove(it->second);
        }
        add(key, val);
    }
    
    int get(int key) {
        
        int val = 0;
        auto it = _H.find(key);
        if(it != _H.end()) {
            val = remove(it->second);
            add(key, val);
            return val;
        }
        return -1;
    }
    
private:
    
    std::list<_kv> _L; //双向链表维护Cache,从头部到尾部优先级降低,即头部优先使用
    std::unordered_map<int, _it_list> _H; //哈希表实现O(1)
    int _cap;
};


2. 有效的括号

  • 题目描述

给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
原题链接

  • 代码
class Solution {
public:
    bool isValid(string s) {

        stack<char> st;
        for(auto ch : s) {
            if(ch == '(')
                st.push(')');
            else if(ch == '{')
                st.push('}');
            else if(ch == '[')
                st.push(']');
            else if(st.empty() || st.top() != ch) //左边括号少或左右括号不匹配
                return false;
            else 
                st.pop();
        }
        return st.empty(); //栈中还有元素的话,就代表左边括号多了
    }
};



3. 后缀表达式

  • 题目描述

根据 逆波兰表示法,求该后缀表达式的计算结果。
有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。
给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
原题链接

  • 代码





4. 表达式求值

  • 题目描述

请写一个整数计算器,支持加减乘三种运算和括号。
原题链接

  • 代码

这题的递归思路和字符串解码类似,难点在于求和栈中的数据如何处理。

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     * 返回表达式的值
     * @param s string字符串 待计算的表达式
     * @return int整型
     */
    int solve(string s) {
        // write code here
        int res = 0;
        stack<int> st;
        char sign = '+';
        int num = 0;
        for(int i = 0; i < s.size(); i ++) {
            if(s[i] >= '0' && s[i] <= '9')
                num = 10 * num + s[i] - '0';
            if(s[i] == '(') {
                int k = i + 1, sum = 0;
                while(sum >= 0) {
                    if(s[k] == '(') sum ++;
                    if(s[k] == ')') sum --;
                    k ++;
                }
                num = solve(s.substr(i + 1, k - i - 2));
                i = k - 1;  //i指向右括号,因为要进行下一轮++
            }
            // 2 - 6 * 7
            if(s[i] == '+' || s[i] == '-' || s[i] == '*' || i == s.size() - 1) {
                if(sign == '+') st.push(num);
                if(sign == '-') st.push(-num);
                if(sign == '*') {
                    auto t = st.top() * num;
                    st.pop();
                    st.push(t);
                }
                sign = s[i];
                num = 0;
            }
        }
        while(!st.empty()) {
           res += st.top();
           st.pop();
        }
        return res;
    }
};





五. 字符串


1. 判断IP地址是否合法

  • 题目描述

原题链接
IPV4地址:

  • 由4组十进制数组成,用’.'分隔;
  • 点和点之间不为空;
  • 长度小于等于3且不能存在前导0;
  • 是整型数字;
  • 其范围为0~255;

IPV6地址:

  • 由8组十六进制数组成,用’:'分隔;
  • 点和点之间不能为空;
  • 长度小于等于4;
  • 每组数的十六进制表示应该合法,且允许大小写字母(a~f 或 A~F)。
  • 代码
class Solution {
public:
    
    vector<string> split(string s, string spliter) {

        vector<string> res;
        int it;
        while((it = s.find(spliter)) && it != s.npos) {
            
            res.push_back(s.substr(0, it));
            s = s.substr(it + 1);
        }
        res.push_back(s);
        return res;
    }

    bool isipv4(string s) {

        vector<string> strs = split(s, ".");
        if(strs.size() != 4) return false;
        for(string &str : strs) {
            if(str.empty()) return false;
            if(str.size() > 3 || (str[0] == '0' && str.size() != 1)) return false;
            for(char &ch : str) if(!isdigit(ch)) return false;
            int num = stoi(str);
            if(num < 0 || num > 255) return false;
        }
        return true;
    }

    bool isipv6(string s) {

        vector<string> strs = split(s, ":");
        if(strs.size() != 8) return false;
        for(string &str : strs) {
            if(str.empty() || str.size() > 4) return false;
            for(char &ch : str) 
                if(!( isdigit(ch) || (ch >= 'a' && ch <= 'f') || 
                (ch >= 'A' && ch <= 'F'))) return false;
        }
        return true;
    }

    string validIPAddress(string IP) {

        if(isipv4(IP)) return "IPv4";
        else if(isipv6(IP)) return "IPv6";
        else return "Neither";
    }
};


2. 字符串出现次数的TopK问题

  • 问题描述

给定一个字符串数组,再给定整数k,请返回出现次数前k名的字符串和对应的次数。
返回的答案应该按字符串出现频率由高到低排序。如果不同的字符串有相同出现频率,按字典序排序。
对于两个字符串,大小关系取决于两个字符串从左到右第一个不同字符的 ASCII 值的大小关系。
比如"ah1x"小于"ahb",“231”<”32“。字符仅包含数字和字母。
[要求]
如果字符串数组长度为N,时间复杂度请达到O(N \log K)O(NlogK)
原题链接

  • 代码

要灵活使用STL的sort(),对于不少排序问题指定好排序规则直接排序即可,真的是很方便

class Solution {
public:
    /**
     * return topK string
     * @param strings string字符串vector strings
     * @param k int整型 the k
     * @return string字符串vector>
     */
    vector<vector<string> > topKstrings(vector<string>& strings, int k) {
        // write code here
        
        unordered_map<string, int> cnt;
        for(auto str : strings) {
            if(cnt.count(str))
                cnt[str] ++;
            else 
                cnt[str] = 1;
        }
        
        typedef pair<string, int> p;
        vector<vector<string>> res;
        for(auto i : cnt) 
            res.push_back({i.first, to_string(i.second)});
        sort(res.begin(), res.end(), [](vector<string> &a, vector<string> &b) {
            
           if(a[1] == b[1]) return a[0] < b[0]; 
           return  stoi(a[1]) > stoi(b[1]);
        });
        
        res.resize(k);
        return res;
    }
};



3. 最长不包含重复字符子串

  • 题目描述

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
原题链接

  • 代码

滑动窗口

class Solution {
public:
    int lengthOfLongestSubstring(string s) {

        vector<bool> st(256, false); //和哈希表一个作用
        int res = 0, l = 0, r = 0;
        while(r < s.size()) {
            while(st[s[r]]) st[s[l ++]] = false; //移动左窗口,直到把重复元素删除
            res = max(res, r - l + 1);
            st[s[r ++]] = true;
        }
        return res;
    }
};



4. 字符串最长公共前缀

  • 题目描述

原题链接

  • 代码

此题迷惑性较强,看起来比较复杂实则不然,写代码前可以先走一遍寻找最长公共前缀的流程。

法一:纵向对比暴力解

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        // write code here
        if(!strs.size()) return "";
        for(int i = 0; i < strs[0].size(); i ++) {  //以第一个字符串为基准就可以
            for(int j = 1; j < strs.size(); j ++) {
                if(i == strs[j].size() || strs[0][i] != strs[j][i])
                    return strs[0].substr(0, i); 
            }
        }
        return strs[0];
    }
};


法二:排序后比较首尾字符串

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        // write code here
        if(strs.empty()) return "";
        sort(strs.begin(), strs.end());
        string first = strs.front(), last = strs.back();
        for(int i = 0; i < first.size(); i ++) {
            if(first[i] != last[i])
                return first.substr(0, i);
        }
        return first;
    }
};


5. 字符串解码

  • 题目描述

给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
原题链接

  • 代码
class Solution {
public:
    string decodeString(string s) {

        string res;
        for(int i = 0; i < s.size(); ) {
            if(!isdigit(s[i])) res += s[i ++];
            else {
                int j = i;
                while(isdigit(s[j])) j ++;
                int num = stoi(s.substr(i, j - i));
                
                int k = j + 1, sum = 0; //找最外层 ] 的位置
                while(sum >= 0) {
                    if(s[k] == '[') sum ++;
                    else if(s[k] == ']') sum --;
                    k ++;
                }
                string str = decodeString(s.substr(j + 1, k - j - 2));
                while(num --)
                    res += str;
                i = k;
            }   
        }
        return res;
    }
};



6. 文本左右对齐

  • 题目描述

此题是荣耀8.21笔试题
原题链接

  • 代码

模拟、字符串拼接、分情况讨论







六. 动态规划


1. 最长递增子序列

  • 题目描述

描述
给定数组arr,设长度为n,输出arr的最长递增子序列。(如果有多个答案,请输出其中 按数值(注:区别于按单个字符的ASCII码值)进行比较的 字典序最小的那个)
原题链接

  • 代码

动态规划 + 二分








2. 最长回文子串

  • 题目描述

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

  • 代码

DP枚举所有子串并确定其是否为回文串,然后过程中更新最大值和起点;

注意,此题必须按长度更新DP数组,如果按下标进行更新dp[i][j] = dp[i+1][j-1]并不是正确结果,因为dp[i+1][j-1]没有被更新过。

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) {
                        dp[i][j] = true;
                    } else {
                        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);
    }
};



3. 01背包

  • 题目描述

现有一个容量大小为V的背包和N件物品,每件物品有两个属性,体积和价值,请问这个背包最多能装价值为多少的物品?
原题链接

  • 代码

dp[i][j]:表示从前i件物品中选且不超过体积j的最大价值;
存在选第i件物品和不选第i件物品两种情况。

#include 
#include 
#include 
using namespace std;

int V, n;
const int N = 20010;
int dp[N][N];
int v[1010], w[1010];

int main() {
    
    cin >> V >> n;
    for(int i = 1; i <= n; i ++) {
        cin >> v[i] >> w[i];
    }
    
    for(int i = 1; i <= n; i ++) {
        for(int j = 0; j <= V; j ++) {
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i])
                dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
    }
    
    cout << dp[n][V] << endl;
    return 0;
}



4. 子数组和的差最小

  • 题目描述

一个数组中有若干正整数,将此数组划分为两个子数组,使得两个子数组各元素之和a,b的差最小,对于非法输入应该输出ERROR。
原题链接

  • 代码

DP做法:此题可以看作容积为sum/2的01背包问题,注意下面的代码和原题并不一致。

/*************************************************************************
	> File Name: exam.cpp
	> Author: ma6174
	> Mail: [email protected] 
	> Created Time: Thu 16 Sep 2021 07:06:31 PM CST
 ************************************************************************/

#include
#include 
using namespace std;

int main() {

	vector<int> arr;
	int a;
	while(cin >> a) {
		arr.push_back(a);
		if(cin.get() == '\n')
			break;
	}
	int s = 0;
	for(auto x : arr) {
		s += x;
	}
	int sum = s / 2;
	int n = arr.size();
	vector<vector<int>> dp(n + 10, vector<int>(n + 10, 0));
	for(int i = 1; i <= n; i ++) {
		for(int j = 0; j <= sum; j ++) {
			dp[i][j] = dp[i - 1][j];
			if(j >= arr[i - 1]) {
				dp[i][j] = max(dp[i][j], dp[i - 1][j - arr[i - 1]] + arr[i - 1]);
			}
		}
	}
	cout << s - dp[n][sum] * 2 << endl;
	return 0;
}

DFS做法:文章链接







5. 零钱兑换(完全背包问题)

  • 题目描述

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

  • 代码

完全背包,二维会超时 -》优化成一维

超时代码如下:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {

        //状态表示dp[i][j]: 从前i种硬币中选,对应的总金额为j的最少硬币数
        //集合划分:枚举第i种硬币选k个(k>=0 && k*val<=j)的所有方案中,取出一个最小的
        const int INF = 0x3f3f3f3f;
        int n = coins.size();
        vector<vector<int>> dp(n + 1, vector<int>(amount + 1, INF));
        dp[0][0] = 0;
        for(int i = 1; i <= n; i ++) {
            int val = coins[i - 1];
            for(int j = 0; j <= amount; j ++) {
                for(int k = 0; k*val <= j; k ++) {
                    dp[i][j] = min(dp[i][j], dp[i - 1][j - k*val] + k);
                }
            }
        }
        if(dp[n][amount] == INF) {
            return -1;
        } else {
            return dp[n][amount];
        }
    }
};

优化后的一维代码:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {

        //状态表示dp[i][j]: 从前i种硬币中选,对应的总金额为j的最少硬币数
        //集合划分:枚举第i种硬币选k个(k>=0 && k*val<=j)的所有方案中,取出一个最小的
        //1. dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - v] + 1, dp[i - 1][j - 2*v] + 2
        //              ... , dp[i - 1][j - k*v] + k)
        //2. dp[i][j - v] + 1= min(dp[i - 1][j - v] + 1, dp[i - 1][j - 2*v] + 2, ... )
        //
        //结合1、2可得:dp[i][j] = min(dp[i - 1][j], dp[i][j - v] + 1)

        const int INF = 0x3f3f3f3f;
        int n = coins.size();
        vector<int> dp(amount + 10, INF);
        dp[0] = 0;
        for(int i = 1; i <= n; i ++) {
            int val = coins[i - 1];
            for(int j = val; j <= amount; j ++) {
                dp[j] = min(dp[j], dp[j - val] + 1);
            }
        }
        if(dp[amount] == INF) {
            return -1;
        } else {
            return dp[amount];
        }
    }
};



七. DFS/BFS


1. 组合总和II

  • 题目描述

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
注意:解集不能包含重复的组合。
原题链接

  • 代码

此题对于dfs不熟练的人还是有难度的,值得注意的是在递归之前进行了一次排序,排序对于后续的处理很重要,减少了相当多的枚举次数同时去重操作也得到了简化:

  • 首先排序之后,当在递归过程中遇到>target的情况的直接返回无需遍历后面的数字;
  • 其次在排序之后重复元素会集中在一起,这样在枚举某种情况时只要前一个元素和当前元素相同就不会再push_back了,非常容易处理。
class Solution {
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {

        sort(candidates.begin(), candidates.end());
        vector<vector<int>> res;
        vector<int> cur;
        dfs(res, cur, target, 0, candidates);
        return res;
    }

    void dfs(vector<vector<int>> &res, vector<int> &cur, int target, 
    int i, vector<int> &candidates) {

        if(target == 0) {
            res.push_back(cur);
            return;
        }

        int j = i;
        for( ; j < candidates.size(); j ++) {

            if(candidates[j] > target)
                return;
            if(j > i && candidates[j] == candidates[j - 1])
                continue;

            cur.push_back(candidates[j]);
            dfs(res, cur, target - candidates[j], j + 1, candidates);
            cur.pop_back();
        }
    }
};


2. 走迷宫

  • 题目描述

输入包含多组数据。
每组数据包含一个10*10,由“#”和“.”组成的迷宫。其中“#”代表墙;“.”代表通路。
入口在第一行第二列;出口在最后一行第九列。
从任意一个“.”点都能一步走到上下左右四个方向的“.”点。
对应每组数据,输出从入口到出口最短需要几步。
原题链接

  • 代码

作为一个算法很菜的非科班选手,此题让我了解到了如何处理多组输入,同时迷宫最短路径问题也是最经典的BFS问题。

// write your code here cpp
#include 
#include 
#include 

using namespace std;

int bfs(vector<vector<char>> &map) {
    
    int n = map.size(), m = map[0].size();
    vector<vector<bool>> st(n, vector<bool>(m, false));
    vector<vector<int>> res(n, vector<int>(m, 0));
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, -1, 0, 1};
    queue<pair<int, int>> q;
    q.push({0,1}); //出口{9,8}
    res[0][1] = 0; 
    
    while(!q.empty()) {
        auto t = q.front();
        q.pop();
        st[t.first][t.second] = true;
        for(int i = 0; i < 4; i ++) {
            int a = t.first + dx[i], b = t.second + dy[i];
            if(a >= 0 && a < n && b >= 0 && b < m 
               && map[a][b] == '.' && !st[a][b]) {
                res[a][b] = res[t.first][t.second] + 1;
                q.push({a,b});
            }
        }
    }
    return res[9][8];
}

int main() {
    
    vector<vector<char>> m(10, vector<char>(10));
    
    while(cin >> m[0][0]) {
        for(int i = 0; i < 10; i ++)
            for(int j = 0; j < 10; j ++){
                if(i == 0 && j == 0) continue;
                cin >> m[i][j];
            }
        cout << bfs(m) << endl;
    }
        
    return 0;
}



3. 全排列

  • 题目描述

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

  • 代码

经典DFS问题

class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {

        vector<vector<int>> res;
        vector<int> tmp;
        vector<bool> st(nums.size(), false);
        dfs(res, tmp, 0, nums, st);
        return res;
    }

    void dfs(vector<vector<int>> &res, vector<int> &tmp, int u, 
    vector<int> &nums, vector<bool> &st) {
        
        if(u == nums.size()) {
            res.push_back(tmp);
            return;
        }

        for(int i = 0; i < nums.size(); i ++) {
            if(!st[i]) {
                tmp.push_back(nums[i]);
                st[i] = true;
                dfs(res, tmp, u + 1, nums, st);
                st[i] = false;
                tmp.pop_back();
            }
        }
    }
};


含有重复元素的全排列问题
原题链接

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        
        res.clear();
        path.clear();
        vector<bool> st(nums.size(), false);
        sort(nums.begin(), nums.end());
        dfs(nums, st);
        return res;
    }

    void dfs(vector<int> &nums, vector<bool> &st) {
        
        if(path.size() == nums.size()) {
            res.push_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); i ++) {
            if(i > 0 && nums[i] == nums[i - 1] && st[i - 1] == false)
                continue;
            if(!st[i]) {
                path.push_back(nums[i]);
                st[i] = true;
                dfs(nums, st);
                st[i] = false;
                path.pop_back();
            }
        }
    }
};



4. 岛屿数量

  • 题目描述

给一个01矩阵,1代表是陆地,0代表海洋, 如果两个1相邻,那么这两个1属于同一个岛。我们只考虑上下左右为相邻。
岛屿: 相邻陆地可以组成一个岛屿(相邻:上下左右) 判断岛屿个数。
原题链接

  • 代码

分块BFS,扫描整个地图遇到0直接跳过,遇到1开始入队BFS,BFS过程中将1周围的所有1感染成0即可。

class Solution {
public:
    /**
     * 判断岛屿数量
     * @param grid char字符型vector> 
     * @return int整型
     */
    int solve(vector<vector<char> >& grid) {
        // write code here
        int res = 0;
        int n = grid.size(), m = grid[0].size();
        int dx[] = {-1, 0, 1, 0}, dy[] = {0, -1, 0, 1};
        for(int i = 0; i < n; i ++) {
            for(int j = 0; j < m; j ++) {
                if(grid[i][j] == '0')
                    continue;
                else {
                    res ++;
                    queue<pair<int, int>> q;
                    q.push(make_pair(i, j));
                    while(q.size()) {
                        auto t = q.front();
                        q.pop();
                        grid[t.first][t.second] = '0';
                        for(int i = 0; i < 4; i ++) {
                            int x = t.first + dx[i], y = t.second + dy[i];
                            if(x >= 0 && x < n && y >= 0 && y < m 
                               && grid[x][y] != '0') {
                                q.push(make_pair(x, y));
                            }
                        }
                    }
                }
            }
        }
        return res;
    }
};






八. 数学


1. 不用循环和条件语句求 n!

  • 题目描述

求 12…*n,要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句 (A?B:C)。
原题链接
原题是加法,一样的

  • 代码
class Solution {
public:
	int getMul(n) {

		int res = n;
		res > 1 && res += getMul(n - 1);
		return res;
	}
}



2. 判断一个数是否为回文数

  • 题目描述

给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。
原题链接

  • 代码

不转换为字符串该如何比较?

class Solution {
public:
    bool isPalindrome(int x) {

        if(x < 0 || (x != 0 && x % 10 == 0)) // 先排除负数或大于0且个位数为0的数
            return false;  

        int renum = 0; // 后半部分数字
        while(x > renum) {
            renum = renum * 10 + x % 10;
            x /= 10;
        }
        
        return x == renum || x == renum / 10; // 1221 和 12321 分别对应的情况都是合法的
    }
};



3. 判断是否为3的幂

  • 题目描述

如题,原题链接

  • 代码

简单题,递归即可

class Solution {
public:
    bool isPowerOfThree(int n) {

        if(n == 0) return false;
        else if(n == 1) return true;
        return isPowerOfThree(n / 3) && (n % 3 == 0);
    }
};



4. 购买钉子

  • 题目描述

小明装修需要n(1 <= n <= 200)颗钉子,但是五金店没有散装钉子卖,只有两种盒装包装的,一种装4颗,另一种装9颗,请问小明最少需要买多少盒钉子才能刚好买够n颗?
荣耀8.21笔试题

  • 代码

此题的原型是零钱兑换问题,而且要简单很多

#include 
using namespace std;

int main() {

	int n;
	cin >> n;
	
	int res = 0;
	if(n % 9 == 0) {
		res = n / 9;
	} else {
		int cnt9 = n / 9;
		int cnt4 = n % 4;
		int i = 0;
		while((cnt4 + i * 9) % 4 != 0 && cnt9) {
			i ++;
			cnt9 --;
		}
		if((cnt4 + i * 9) % 4 == 0) {
			res = cnt9 + (cnt4 + i * 9) / 4;
		} else {
			res = -1;
		}
	}
	cout << res << endl;
	return 0;
}





九. 二分/双指针/位运算


1. 寻找递增递减序列的峰值

  • 题目描述

如题
原题链接
注意,leetcode上的题目和本题有一点区别。

  • 代码

二分:寻找两个不同性质的序列的分界点

class Solution {
public:
	int maxIndex(vector<int> &arr) {

		int l = 0, r = arr.size() - 1;
		while(l < r) {
			int mid = l + r >> 1;
			if(arr[mid] > arr[mid + 1])
				r = mid;
			else 
				l = mid + 1;
		}
		return l;
	}
}





33. 接雨水

  • 题目描述

原题链接

  • 代码




31.

  • 题目描述

原题链接

  • 代码




31.

  • 题目描述

原题链接

  • 代码




31.

  • 题目描述

原题链接

  • 代码




31.

  • 题目描述

原题链接

  • 代码




31.

  • 题目描述

原题链接

  • 代码




31.

  • 题目描述

原题链接

  • 代码




你可能感兴趣的:(数据结构与算法(基础篇),算法,leetcode)