算法笔记C++版本(持续更新)

算法笔记C++版本(持续更新)

当前背景:准备研究生复试,重头学习C++用于机考
笔记内容:算法题目思路和答案
学习参考书籍《Hello算法》C\C++版本,《LeetCode101:A LeetCode Grinding Guide》
代码题目均来自leetcode hot-100

数组

16. 最接近的三数之和

双指针实现,唯一注意的点就是aim值写法和minus值的写法

class Solution {
public:
    int threeSumClosest(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end());
        int ans;
        int closeDis = INT_MAX;
        for (int i = 0; i < nums.size() - 2; i++) {
            int left = i + 1;
            int right = nums.size() - 1;
            int aim = target - nums[i];
            while (left < right) {
                int temp = nums[left] + nums[right];
                int minus = abs(aim - temp);
                if (minus < closeDis) {
                    ans = temp + nums[i];
                    closeDis = minus;
                }
                if (minus == 0) {
                    break;
                }
                if (temp < aim) {
                    left++;
                } else {
                    right--;
                }
            }
                if (closeDis == 0) {
                    break;
                }
        }
        return ans;
    }
};

双指针

前后遍历双指针

快慢指针

142.环形链表 2

思想:fast、slow指针,步长分别为2、1,相遇后slow不变,fast回到起点,按照步长1、1遍历,相遇即为链表环形起点。
如何理解??
答:第一次看到这个解决方案十分不理解,为什么第二次fast要回到起点,并且下次相遇就是起点。 这里不要去纠结fast的变化,而是理解他们第一次相遇的位置具有什么特别之处(都是在有环的情况下讨论)
首先,fast走的路程一定比slow多n个环,可以写出等式: fast=slow+np。fast的步长是slow的两倍,得出等式:fast=2slow,所以可以推到出slow=np。
其次,因为slow的总路程是np,这一段路程包含直线路程,所以slow走的n个环,有直线路程k在里面,不是完整的n个环路程。
最后,根据上面两个就可以得到:slow再往后走k次就能到环起点。k怎么确定?只需要再用一个步长为1的指针和slow同步移动,相遇时即为环起点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* detectCycle(ListNode* head) {
    //用一个空头结点作为起点,第二次循环时才能保证正确。
        ListNode* tmpHead= new ListNode(0);
        tmpHead->next = head;
        ListNode* fast = tmpHead;
        ListNode* slow = tmpHead;
        while (fast != NULL) {
        //跳跃两步一定要避免访问空指针
            if (fast->next != NULL) {
                fast = fast->next->next;
            } else {
                fast = fast->next;
            }
            slow = slow->next;
            if(fast==slow){
                break;
            }
        }
        if (fast == NULL) {
            return NULL;
        }
        //从空头结点开始往后,次数保证正确。
        fast = tmpHead;
        while (fast != slow) {
            fast = fast->next;
            slow = slow->next;
        }
        delete(tmpHead);
        return fast;
    }
};
234.回文链表

这是一道简单题,但是考察链表两种操作,需要注意较多细节
思路:
1.使用快慢指针遍历到中间节点,遍历条件为fast不为nullptr并且fast->next不为nullptr, 中间节点分为两种情况:1)节点为偶数,slow停在前半部分的最后一个节点。2)节点为奇数,slow停在正中间节点。
2.反转链表:以slow为第一个前驱pre节点,依次把slow->next后续的所有节点next指向前一个。
3.从链表头尾开始用fast、slow往中间比较,比较到fast为空时结束。

注意:当对后半部分反转时,需要把slow的next置为空,如果不置空会导致slow和slow->next形成一个环,程序结束释放节点时会导致错误:heap-use-after-free on address。这种错误要么是使用已经释放空间的指针,要么对某个指针进行重复释放,本题属于第二种情况。

性能方面:采用step记录节点数量,在后续比较时使用,这种效率较低,改成指针遍历到空会更优

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        ListNode* slow = head;
        ListNode* fast = slow->next;
        //int step = 1;
        //快慢指针找到中间节点
        while(fast != nullptr && fast->next != nullptr) {
            fast = fast->next->next;
            slow = slow->next;
            //step++;
        }
        ListNode* cur = slow->next;
        //断开链表,避免出现循环
        slow->next =nullptr;
        ListNode* temp = nullptr;
        //反转后半部分, 1->2->2->1  变成 1->2->null  和1->2<-2<-1
        while(cur) {
            temp = cur->next;
            cur->next = slow;
            slow = cur;
            cur = temp;
        }
        fast = head;
        while(fast) {
            if(fast->val == slow->val) {
                fast = fast->next;
                slow = slow->next;
                //step--;
            } else {
                return false;
            }
        }
        return true;
    }
};

DFS深度遍历(重点)

深度和广度是上机考试的重点!一般结合图的形式考察(岛屿问题)

100.相同的树(简单)

思路:考察树的遍历,使用前序遍历。只需要注意null的处理和bool返回即可

/**
 * 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:
    bool isSameTree(TreeNode* p, TreeNode* q) {
        return dfs(p,q);
    }

    bool dfs(TreeNode* q, TreeNode* p) {
        //先把空指针处理完
        if(q == nullptr && p == nullptr) {
            return true;
        }
        if(q == nullptr || p == nullptr) {
            return false;
        }

        if(q->val != p->val) {
            return false;
        }
        //可以美化下面注释的代码
        // bool isSame = dfs(q->left,p->left);
        // if(isSame) {
        //     return dfs(q->right,p->right);
        // }
        // return false;

        return dfs(q->left,p->left) && dfs(q->right,p->right);
    }
};

129. 求根节点到叶节点数字之和

/**
 * 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 {
    int sumValue = 0;
public:
    int sumNumbers(TreeNode* root) {
        dfs(root, 0);
        return sumValue;
    }

    void dfs(TreeNode* node, int traceSum){
        if(node == nullptr) {
            return;
        }
        if(node->left == nullptr && node->right == nullptr) {
            node->val += traceSum*10;
            sumValue += node->val;
            return;
        }

        node->val += traceSum*10;
        traceSum = node->val;
        
        dfs(node->left, traceSum);
        dfs(node->right, traceSum);
        return;
    }
};

BFS广度遍历(重点)

贪心算法

每次操作选取当前的局部最优解,最终达到全局最优

445.分发饼干

思路:对胃口和饼干数组进行升序排序,顺序查找第一个大于等于胃口的饼干(这一步就是贪心的方式),然后找下个胃口满足条件的饼干。
注意:
1.找满足下个胃口的饼干时,饼干遍历的位置应该是位于上一个被吃掉的饼干位置+1,能有效提升效率。
2.为什么每次只找一个满足大于等于胃口数量的饼干? 答:因为题目限制每个孩子只能投喂一块饼干,不存在用两个1的饼干满足胃口2的孩子。

//孩子胃口值和饼干尺寸是否有序? sort排序
class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(s.begin(),s.end());
        sort(g.begin(),g.end());
        int ans = 0;
        int cookId = 0;
        for(int i = 0; i < g.size(); i++) {
            for(int j = cookId; j < s.size(); j++) {
                cookId = j+1;
                //诶 找到满足孩子胃口的饼干了,ans记录饼干数量,并且下次从j+1的位置找下一个饼干。
                if(s[j] >=  g[i]) {
                    ans++;
                    s[j] = 0;
                    break;
                }
            }
        }
        return ans;
    }
};

435 无重叠区间

思路:区间末端是重点,按照末端进行排序,从最小的开始往后找,能保证剩余的区间被覆盖的面积最小,比如[1,3] [1,2] [1,4] [2,3] ,如果从末端排序查找,[1,2]能覆盖1~4区间的最小区域,剩余区域有其他区间覆盖的可能性最大,要去除的区间数量就会最少(贪心,贪小的区间,能够保留的区间数量最大。
注意二维数组排序方法,sort的第三个比较参数用lambda,lambda内是比较元素即子向量,所以是一维数组为参数,比较第二个元素。

完全没想到怎么做。。。背一下记录一下该题,做人还是不够贪心

//选择尾区更小的进行union,保证剩余空间最大(贪心)
class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), [](vector<int>& a, vector<int>& b){return a[1] < b[1];});
        int ans = 0;
        int left = 0;
        for(int i = 0; i < intervals.size()-1;i++) {
            if(intervals[left][1] > intervals[i+1][0]) {
                ans++;
                continue;
            } 
            left = i+1;
        }
        return ans;
    }
};

135.分发糖果

思路:从左往右遍历一遍,评分比左边高的,糖果数量等于左边数量+1;
再从右往左遍历一遍,评分比右边高,并且糖果数量小于等于右边的,糖果数量改为右边数量+1

不会,不懂,只觉得这个方法牛逼,下次遇到相邻对象比较的可以尝试这个方法。

class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> numsCandy(ratings.size(),1);
        for(int i =1; i < ratings.size(); i++) {
            if(ratings[i] > ratings[i-1]) {
                numsCandy[i] = numsCandy[i-1] +1;
            }
        }
        for(int i = ratings.size()-2; i >=0; i--) {
            if(ratings[i] > ratings[i+1] && numsCandy[i] <= numsCandy[i+1]) {
                numsCandy[i] = numsCandy[i+1] + 1;
            }
        }
        int ans = 0;
        for(int& num : numsCandy) {
            ans = ans + num;
        }
        return ans;
    }
};

动态规划

动态规划问题的特性在于无后效性,即未来发展只与当前状态有关,和所有历史经历无关。
动态规划、回溯、分治问题的特性:
在这里插入图片描述

入门:爬楼梯问题

介绍三种方法,逐步引出动态规划的思想。

方法一,暴力搜索
采用递归的方式,分解原问题成为两个子问题dp[n] = dp[n-1]+dp[n-2]

int dfs(int i) {
// 已知 dp[1] 和 dp[2] ,返回之
if (i == 1 || i == 2)
return i;
// dp[i] = dp[i-1] + dp[i-2]
int count = dfs(i - 1) + dfs(i - 2);
return count;
}
/* 爬楼梯:搜索 */
int climbingStairsDFS(int n) {
return dfs(n);
}

在这里插入图片描述
从实际执行的二叉树中可以看出,存在大量重复计算,时间复杂度很高,这里引出一个优化方案:记忆搜素。把已经计算过的dp[n]保存下来,下次重复执行时,直接取出结果而不用重复计算(这种方法也可以成为剪枝)

方法二记忆搜索

/* 记忆化搜索 */
int dfs(int i, vector<int> &mem) {
// 已知 dp[1] 和 dp[2] ,返回之
	if (i == 1 || i == 2)
	return i;
// 若存在记录 dp[i] ,则直接返回之
	if (mem[i] != -1)
	return mem[i];
// dp[i] = dp[i-1] + dp[i-2]
	int count = dfs(i - 1, mem) + dfs(i - 2, mem);
// 记录 dp[i]
	mem[i] = count;
	return count;
}
/* 爬楼梯:记忆化搜索 */
int climbingStairsDFSMem(int n) {
// mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录
vector<int> mem(n + 1, -1);
return dfs(n, mem);
}

在这里插入图片描述

方法三动态规划
自底向顶,迭代解决问题,从最小的子问题开始到最终解
dp[3] = dp[2]+dp[1] ; dp[4] = dp[3]+dp[2] ; …

/* 爬楼梯:动态规划 */
int climbingStairsDP(int n) {
	if (n == 1 || n == 2)
	return n;
// 初始化 dp 表,用于存储子问题的解
	vector<int> dp(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];
}

322.零钱兑换

类似背包问题,都是容量、物品价值二维问题。
背包问题求解思路如下:
1.需要从每个物品开始,遍历一遍容量由1到最大。
2.判断当前物品装入和不装入哪个总价值更大,不装入时的价值等于上一个物品在当前容量的最大价值。
3.更新到容量最大,物品最后一个,就得到当前背包所能装的最大价值。

零钱兑换思路如下:[coin, money]
1.从每个硬币遍历一次总金额从1到目标值的情况。
2.当前硬币大于总金额则把上个硬币的情况[coin-1,money]赋值给当前的[coin,money]。
3.当前硬币小于等于总金额则计算min([coin-1,money],[coin,money-[coin-1]]+1)

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<vector<int>> dp(coins.size()+1,vector<int>(amount+1,0));
        int maxFlag = amount+1;
        for(int i = 0; i <= coins.size(); i++) {
            dp[i][0] = 0;
        }
        
        for(int i = 1; i <= amount; i++) {
            dp[0][i] = maxFlag;
        }

        for(int i = 1; i <= coins.size(); i++) {
            for(int money = 1; money <= amount; money++) {
                if(coins[i-1] > money) {
                    dp[i][money] = dp[i-1][money];
                } else {
                    dp[i][money] = min(dp[i-1][money],1 + dp[i][money-coins[i-1]]);
                }
            }
        }
        if(dp[coins.size()][amount] == maxFlag) {
            return -1;
        }
        return dp[coins.size()][amount];
    }
};

空间优化

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, 0);
        int maxFlag = amount + 1;

        for (int i = 1; i <= amount; i++) {
            dp[i] = maxFlag;
        }
        for (int i = 1; i <= coins.size(); i++) {
            for (int money = 1; money <= amount; money++) {
                if (coins[i - 1] > money) {
                    dp[money] = dp[money];
                } else {
                    dp[money] = min(dp[money], 1 + dp[money - coins[i - 1]]);
                }
            }
        }

        if (dp[amount] == maxFlag) {
            return -1;
        }
        return dp[amount];
    }
};
总结

动态规划:从最小子问题的解开始,迭代地构建更大子问题的解,直 至得到原问题的解;无后效性是动态规划能够有效解决问题的重要特性之一,其定义为:给定一个确定的状态,它的未来发展只 与当前状态有关,而与过去经历的所有状态无关。
1. 如何判断问题是否为动态规划:
总的来说,如果一个问题包含重叠子问题、最优子结构,并满足无后效性,那么它通常适合用动态规划求解。 然而,我们很难从问题描述中直接提取出这些特性。因此我们通常会放宽条件,先观察问题是否适合使用回 溯(穷举)解决。适合用回溯解决的问题通常满足“决策树模型”,这种问题可以使用树形结构来描述,其中每一个节点代表 一个决策,每一条路径代表一个决策序列。换句话说,如果问题包含明确的决策概念,并且解是通过一系列决策产生的,那么它就满足决策树模型,通常可以使用回溯来解决。 在此基础上,动态规划问题还有一些判断的“加分项”。
‧ 问题包含最大(小)或最多(少)等最优化描述。
‧ 问题的状态能够使用一个列表、多维矩阵或树来表示,并且一个状态与其周围的状态存在递推关系。
相应地,也存在一些“减分项”。
‧ 问题的目标是找出所有可能的解决方案,而不是找出最优解。
‧ 问题描述中有明显的排列组合的特征,需要返回具体的多个方案。
如果一个问题满足决策树模型,并具有较为明显的“加分项”,我们就可以假设它是一个动态规划问题,并在 求解过程中验证它。
2. 问题求解步骤:描述决策,定义状态,建立 dp 表,推导状态转移方程,确定边界条件等。

回溯

1. 思路:类似于DFS,每次遍历需要把路径上的值记录到path,每次退出当前节点后需要把该节点存在path的值弹出。
2. 框架:(1)模拟画出树结构,写出每轮保存值后path值对应多少(2)确定递归终止条件:一般是path的长度等于预定长度,或者遍历完总对象(3)写backtracking函数
3. 分类:遍历对象是否可以重复访问,可用visit记录,或者从上次访问对象顺序往后访问,则用index做参数。

46. 全排列

题目:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

//终止条件:ans长度等于n
//遍历对象:每次从除自身外的遍历,需要用到visit做记录,退出时重置false
class Solution {
    vector<vector<int>> res;
    vector<int> path;
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> visit(nums.size(),false);
        backtracking(nums, visit);
        return res;
    }
    void backtracking(vector<int>& nums, vector<bool>& visit) {
        if(path.size()==nums.size()){
            res.emplace_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); i++) {
            if(visit[i]) {
                continue;
            }
            path.emplace_back(nums[i]);
            visit[i] = true;
            backtracking(nums,visit);
            visit[i] = false;
            path.pop_back();
        }
    }
};

78. 子集

题目:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

//终止条件:for循环天然实现终止
//剪枝:传递参数index,每次只能遍历i+1之后的元素
class Solution {
    vector<vector<int>> res;
    vector<int> path;
public:
    
    vector<vector<int>> subsets(vector<int>& nums) {
        backtracking(nums, 0);
        return res;
    }
    void backtracking(vector<int>& nums,int index) {
        res.emplace_back(path);
        for(int i = index; i < nums.size(); i++) {
            path.emplace_back(nums[i]);
            backtracking(nums, i+1);
            path.pop_back();
        }
    }

};

17. 电话号码的字母组合

题目:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]

class Solution {
public:
    vector<string> ans;
    string path;
    unordered_map<char, string> map;
    vector<string> letterCombinations(string digits) {
        map.insert({'2', "abc"});
        map.insert({'3', "def"});
        map.insert({'4', "ghi"});
        map.insert({'5', "jkl"});
        map.insert({'6', "mno"});
        map.insert({'7', "pqrs"});
        map.insert({'8', "tuv"});
        map.insert({'9', "wxyz"});
        if(digits.size()==0) {
            return {};
        }
       
        backtracking(digits, 0);
        return ans;
    }
    void backtracking(string digits, int index) {
        if (index == digits.size()) {
            ans.push_back(path);
            return;
        }
        string letter = map[digits[index]];
        for (int i = 0 ; i < letter.size();i++) {
            path.push_back(letter[i]);
            backtracking(digits, index+1);
            path.pop_back();
        }
    }
};

39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

class Solution {
public:
//边界条件:ans等于target则加入res,大于则不加入,但终止回溯,小于则继续回溯
//回溯前ans加上当前值, 回溯后ans减去当前值,循环下一个。
    int ans;
    vector<int> tempRes;
    vector<vector<int>> res;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtracking(candidates, target,0);
        return res;
    }
    void backtracking(vector<int>& candidates,int target, int index) {
        if(ans > target) {
            return;
        }
        if(ans == target) {
            res.push_back(tempRes);
            return;
        }
        for(int i = index; i < candidates.size(); i++) {
            ans = ans+candidates[i];
            tempRes.push_back(candidates[i]);
            backtracking(candidates, target, i);
            tempRes.pop_back();
            ans = ans-candidates[i];
        }
    }
};

22. 括号生成

题目:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]

// 限制条件,(1)左括号小于n个(2)右括号小于n个且小于等于左括号个数
// 回溯终止条件:ans.size() == 2n
// 遍历条件与限制条件相同。
class Solution {
public:
    string ans;
    vector<string> res;
    vector<string> generateParenthesis(int n) {
        backtracking(n, 0, 0);
        return res;
    }
    void backtracking(int n, int lNum, int rNum) {
        if (ans.size() == 2 * n) {
            res.push_back(ans);
            return;
        }
        //下面的for循环和两个并列if等价
        // if (lNum < n) {
        //     ans.append("(");
        //     backtracking(n, lNum + 1, rNum);
        //     ans.erase(ans.end() - 1, ans.end());
        // }
        // if (rNum < n && rNum < lNum) {
        //     ans.append(")");
        //     backtracking(n, lNum, rNum + 1);
        //     ans.erase(ans.end() - 1, ans.end());
        // }
        for (int i = 0; i < 2; i++) {
            if (i==0 && lNum < n) {
                ans.append("(");
                backtracking(n, lNum + 1, rNum);
                ans.erase(ans.end()-1,ans.end());
            }
            if(i == 1 && rNum < n && rNum < lNum) {
                ans.append(")");
                backtracking(n, lNum, rNum+1);
                ans.erase(ans.end()-1,ans.end());
            }
        }
    }
};

分治

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值