C++ : 力扣_Top(124-147)

C++ : 力扣_Top(124-147)

文章目录

  • C++ : 力扣_Top(124-147)
      • 124、二叉树中的最大路径和(困难)
      • 125、验证回文串(简单)
      • 127、单词接龙(中等)
      • 128、最长连续序列(困难)
      • 130、被围绕的区域(中等)
      • 131、分割回文串(中等)
      • 134、加油站(中等)
      • 136、只出现一次的数字(简单)
      • 138、复制带随机指针的链表(中等)
      • 139、单次拆分(中等)
      • 140、单词拆分II(困难)
      • 141、环形链表(简单)
      • 147、LRU缓存机制(LRU页面置换算法,中等)


124、二叉树中的最大路径和(困难)

给定一个非空二叉树,返回其最大路径和。
本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。

输入: [1,2,3]

       1
      / \
     2   3

输出: 6

输入: [-10,9,20,null,null,15,7]

   -10
   / \
  9  20
    /  \
   15   7

输出: 42

class Solution {
    int val = INT_MIN; // 最大路径
public:
    int maxPathSum(TreeNode* root){
        if(!root) return 0;
        PathSum(root);
        return val; // 最后返回的不能是res2了,需要是最终的最大值
    }
    int PathSum(TreeNode* root){
        if(!root) return 0;
        int left = PathSum(root->left); // 遍历左子树
        int right = PathSum(root->right); // 遍历右子树
        // 该节点遍历完要返回上一个节点时更新结果:
        int res1 = root->val + max(0,left) + max(0,right); // bac路径和的结果
        int res2 = root->val + max(0, max(left, right)); // bax或cax的结果,x是a的父节点
        val = max(val, max(res1, res2)); // 更新最大路径值的值
        return res2; // 返回的是bax或cax这种的结果!!bac路径的结果不能返回,因为路径中没有a的父节点
    }
};

思路:有趣的题,感觉十分复杂,一开始做的时候思路错了,但如果缕清正确的思路,就会变得比较简单;二叉树最简单的模型abc,a是根结点(递归中当前遍历节点),bc是左右子结点(代表其递归后的最优解),其结果又三种情况:
b + a + c -> res1
b + a + x -> res2
a + c + x -> res2 (其中x表示a的父节点)
情况 1,表示如果不联络父结点的情况,或本身是根结点的情况。这种情况是没法递归的,但是结果有可能是全局最大路径和。
情况 2 和 3,递归时计算 a+b 和 a+c,选择一个更优的方案返回,也就是上面说的递归后的最优解。
另外结点有可能是负值,最大和肯定就要想办法舍弃负值(使用max)。
但是上面 3 种情况,无论哪种,a 作为联络点,都不能够舍弃。

另外,代码中需要注意返回值的问题,该递归代码的返回值不是最终的结果值,需要尤其注意,详见代码注释;


125、验证回文串(简单)

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。说明:本题中,我们将空字符串定义为有效的回文串。

输入: “A man, a plan, a canal: Panama”
输出: true

输入: “race a car”
输出: false

class Solution {
public:
    bool isPalindrome(string s) {
        if(s.empty()) return true;
        int left = 0, right = s.size()-1;
        while(left<right){ // 双指针两头遍历
            while( left<right &&  
                ( s[left]<48||s[left]>122
                || (s[left]>57&&s[left]<65)
                || (s[left]>90&&s[left]<97) ) )
            {
                ++left; // left进行移动直到遇到数字0-9或字母A-Z、a-z
            }
            while( left<right &&
                ( s[right]<48||s[right]>122
                || (s[right]>57&&s[right]<65)
                || (s[right]>90&&s[right]<97) ) )
            {
                --right; // right进行移动直到遇到数字0-9或字母A-Z、a-z
            }
            if(left>=right) break; // 移动后如果到头了,则直接结束循环
            if( (s[left]>=97&&s[right]+32==s[left])
                || (s[right]>=97&&s[left]+32==s[right]) )
            {
                ++left; --right; continue; // 如果其中一个是大写,一个是小写但相等时
            }
            else if(s[left]!=s[right]) return false; // 其他情况,两者不相等时错误
            else{
                ++left; --right; // 记得移动双指针
            }
        }
        return true;
    }
};

思路:简单的题,利用双指针从两头往中间一个一个判断,需要牢记ASCII码中数字、大写字母、小写字符的数字下标分别是48、65、97;仔细思考判断条件和边界条件;除此之外,也可以灵活运用C++的函数来解决问题,但必要性不大;

islower(char c) 是否为小写字母
isuppper(char c) 是否为大写字母
isdigit(char c) 是否为数字
isalpha(char c) 是否为字母
isalnum(char c) 是否为字母或者数字
toupper(char c) 字母小转大
tolower(char c) 字母大转小

127、单词接龙(中等)

给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:

每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
说明:

如果不存在这样的转换序列,返回 0。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。

输入:
beginWord = “hit”,
endWord = “cog”,
wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
输出: 5

解释: 一个最短转换序列是 “hit” -> “hot” -> “dot” -> “dog” -> “cog”,
返回它的长度 5。

输入:
beginWord = “hit”
endWord = “cog”
wordList = [“hot”,“dot”,“dog”,“lot”,“log”]
输出: 0

解释: endWord “cog” 不在字典中,所以无法进行转换。

class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        unordered_set<string> wordDict(wordList.begin(),wordList.end()); // 将词典转化为set容器
        if(wordDict.find(endWord)==wordDict.end()) return 0; // 词典中直接没有endWord,无法转换
        unordered_set<string> beginSet{beginWord}; // 创建beginWord端的set,存放要转换的当前状态stirng
        unordered_set<string> endSet{endWord}; // 创建endWord端的set,存放要转换到的目标状态string
        int step = 1;
        for(; !beginSet.empty(); ){ // 开始BFS双端搜索,直到beginWord端的set为空
            unordered_set<string> tempSet; // 存储当前替换一个字符到下一个string的可行状态
            ++step; // 新的循环,转换步数加1
            for(auto s : beginSet){ // 遍历beginWord端的set,将其中出现过的string在词典中抹除(不重走这个状态)
                wordDict.erase(s); // 转换过的状态string从词典中抹除
            }
            for(auto s : beginSet){ // 遍历循环beginWord端的set,计算这些string状态有多少种允许的下一个状态
                for(int i=0; i<s.size(); ++i){ // 遍历一个string
                    string str = s;
                    for(char c='a'; c<='z'; ++c){ 
                        str[i] = c; // 将string的该位字符替换
                        if(wordDict.find(str)==wordDict.end()){ // 替换后的string不在词典内
                            continue; // 继续字符a-z的循环尝试
                        }         
                        if(endSet.find(str)!=endSet.end()){ // 如果替换后的string直接等于了最终的结果
                            return step; // 返回这一路的步数即可
                        }
                        tempSet.insert(str); // 如果当前是一种词典内的中间状态,则存入暂时的tempSet;
                    }
                }
            }
            if(tempSet.size()<endSet.size()){ // 如果当前这一批状态可以转换到的下一个状态的方法数目小于从另一端转换时的方法数目
                beginSet = tempSet; // 继续从beginSet这一端开始转换,更新当前状态
            }
            else{ // 如果当前端的转换方法种数比从另一端开始要多,则直接从另一端开始转换
                beginSet = endSet; // 准备进入新的转换状态,将当前另一端的状态当做下一次转换的状态
                endSet = tempSet; // 将这一次循环中转换后的几个状态当做下一次都另一端开始转换时的目标状态
            }
        }
        return 0;
    }
};

思路:这道题看起来不是很难,但其实很复杂,主要复杂在代码的编写上,因为很多功能的实现非常繁琐,需要大量思考和代码数目;该题的主要思路其实就是不断的遍历寻找转换状态,每次转换就是替换当前这些状态的其中一个字符,当前状态的存储可以使用set实现,说白了就是一个BFS广度优先遍历的过程;但需要注意的是,由于题目给定了词典,所以该题用普通的BFS还会超时,所以引入了一个挺重要的思想就是双端BFS,两端搜索也就是说:一头从beginWord转换为endWord,另外一头从endWord转换为beginWord。(其中beginWord和endWord表示当前字符串的一些中间状态的集合)原因从一个例子说明:

假设从beginWord转换为endWord,存在于字典中的,(第一个)中间结果有30个。
而从endWord转换为beginWord,存在于字典中的,(第一个)中间结果只有2个。

那么,很显然。从endWord开始会更快。所以,每次都从个数少的那块开始替换一位。所有当前已经遍历到的状态就从词典中删去,因此,我们每次都从中间结果少的那一端出发,这样就能剪枝掉很多不必要的搜索过程。具体编码过程见代码注释;


128、最长连续序列(困难)

给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。

输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        if(nums.empty()) return 0;
        int size = nums.size();
        int result = 1, len = 1;
        unordered_set<int> s(nums.begin(),nums.end()); // 将数组转换为哈希表
        for(auto i : s){
            if( i==INT_MIN || s.find(i-1)==s.end() ){ // 如果当前元素是INT_MIN或一段连续值中的最小数字,则统计该序列长度
                int val = i;
                while( val==INT_MAX || s.find(val+1)!=s.end() ){ // 还存在比当前元素更大的连续元素,更新序列长度
                    if(val==INT_MAX) break; // 本身就是最大值了,不用再加了
                    else{
                        ++len; // 序列长度+1
                        ++val; // val的值+1
                    }
                }
                result = max(result, len); // 更新最大长度
                len = 1; // 重置len
            }
            else{ // 当前不是一段序列的最小元素
                continue; // 不管,继续循环
            }
        }
        return result;
    }
};

思路:该题的难点在于只能在O(n)的时间复杂度内完成,肯定不能用排序的方法了,O(n)的复杂度要求其实暗示要使用哈希表的方法;首先将原本的数组改成哈希表set,然后遍历该哈希表,如果找到了比当前元素小1的数,说明该元素不是某个序列的最小值,则继续遍历;如果找到某个元素没有比它小1的数,则说明他就是m某个序列最小的数val,于是依次寻找存不存在val+1这个元素,如果有,则序列长度也加1;在统计完每个序列的长度时更新最大值;需要注意INT_MIN和INT_MAX的问题,因为这两个数说明已经到达序列的最大最小值;


130、被围绕的区域(中等)

给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。

X X X X
X O O X
X X O X
X O X X

运行函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X

class Solution {
public:
    void solve(vector<vector<char>>& board) {
        if(board.empty()||board[0].empty()) return;
        int size_i = board.size(), size_j = board[0].size();
        for(int i=0; i<size_i; ++i){ // 遍历矩阵第一列和最后一列
            if(board[i][0]=='O') FillPath(board,i,0);
            if(board[i][size_j-1]=='O') FillPath(board,i,size_j-1);
        }
        for(int j=1; j<size_j-1; ++j){ // 遍历矩阵第一行和最后一行
            if(board[0][j]=='O') FillPath(board,0,j);
            if(board[size_i-1][j]=='O') FillPath(board,size_i-1,j);
        }
        for(int i=0; i<size_i; ++i){ // 遍历矩阵,将'O'改为'X',将'1'改为'O'
            for(int j=0; j<size_j; ++j){
                if(board[i][j]=='O') board[i][j] = 'X';
                if(board[i][j]=='1') board[i][j] = 'O';
            }
        }
    }
    void FillPath(vector<vector<char> >& board, int i, int j){ // 深度优先搜索'O'的路径
        if(i<0||i>=board.size()||j<0||j>=board[0].size()||board[i][j]!='O') return;
        board[i][j] = '1';
        FillPath(board,i+1,j);
        FillPath(board,i-1,j);
        FillPath(board,i,j+1);
        FillPath(board,i,j-1);
    }
};

思路:被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的 ‘O’ 最终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。如果遍历数组时遇到’O’就判断他是不是与边界的’O’相连,还是比较复杂的,所以可以取个巧:先遍历矩阵的四条边,如果有’O’,则对这个’O’进行深度搜寻路径,将与其相连的’O’先置换为’1’,然后遍历整个数组,将剩下的’O’变成’X’,将’1’变回’O’;


131、分割回文串(中等)

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。

输入: “aab”
输出:
[“aa”,“b”],
[“a”,“a”,“b”]

class Solution {
    vector<vector<string> > result;
public:
    vector<vector<string>> partition(string s) {
        if(s.empty()) return {}; 
        vector<string> temp; // 存储当前组合结果的容器
        FindCombination(s, 0, s.size()-1, temp); // 进入递归(DFS)
        return result;
    }
    void FindCombination(string& s, int start, int size, vector<string>& temp){
        if(start>size){ // 当前的下标越界了,说明当前的深度优先搜索到头了
            result.push_back(temp); // 将这次的组合结果存入result
            return;
        }
        for(int i=start; i<=size; ++i){ // 当前没到头,继续组合
            if(CheckString(s, start, i)){ // 如果当前的是一个回文串
                temp.push_back(s.substr(start, i-start+1)); // 将这个回文串放入组合中
                FindCombination(s, i+1, size, temp); // 当前组合完,从回文串的下一个字符开始寻找后面的组合 
                temp.pop_back(); // 将这个回文串弹出,探索其他组合可能
            }
        }
    }
    bool CheckString(string& s, int left, int right){ // 检查回文串的函数
        if(left>right) return false; 
        while(left<=right){
            if(s[left++]!=s[right--]) return false;
        }
        return true;
    }
};

思路:既然是要找到所有的组合,那么就要优先考虑DFS,这道题其实本质上还是求一个字符串的所有子串的组合,但只是多了一个条件(成员只能是回文串),但代码思路和递归DFS求全组合一致。递归的中间注意多了一个判断回文的条件函数和跳过当前回文长度的小改动;需要清晰地明白整个递归过程;也要记得全排列、全组合的常规做法;


134、加油站(中等)

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

说明:

如果题目有解,该答案即为唯一答案。
输入数组均为非空数组,且长度相同。
输入数组中的元素均为非负数。

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

解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

输入:
gas = [2,3,4]
cost = [3,4,3]
输出: -1

解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        if(gas.empty()||cost.empty()||gas.size()!=cost.size()) return -1;
        int size = gas.size();
        bool turnback = false;
        for(int i=0; i<size; ++i){ // 遍历所有节点
            if(gas[i]>=cost[i]){ // 当前节点可以出发,则进行判断
                turnback = false; // 重置返回标记
                int nowpos = i+1; // 当前位置为出发后的下一节点下标
                if(nowpos>=size){ turnback = true; nowpos = 0; } 
                int nowgas = gas[i] - cost[i]; // 当前汽油量为出发到下一节点处的汽油量
                while(nowpos!=i){ // 开始循环判断,直到返回出发节点
                    nowgas = nowgas+gas[nowpos]-cost[nowpos]; // 跑到下个节点时的汽油量
                    if(nowgas<0){ // 如汽油不够,无法继续
                        if(!turnback) i = nowpos; // (重要!)从起点i到当前节点之间的所有节点都不能顺利走过
                        break; // 跳出while
                    }
                    ++nowpos; // 当前还有汽油,继续前进 
                    // 越界时,返回头结点, 标记一个返回的标记,这样如果路不通时最开始的遍历节点不用返回
                    if(nowpos>=size){ turnback = true;  nowpos = 0; }
                }
                if(turnback && nowpos==i) return i; // 如果循环完回到起点了,则成功,返回起点下标
            }
        }
        return -1;
    }
};

思路:该题利用暴力搜索法可以比较容易的写出代码,以每个节点为起始点进行循环判断即可;另一种更好的优化办法是必须发现这么一个规律:”如果以某个节点i出发,走到节点j时无法继续行进时,则说明从i到j的所有节点都不能作为起始节点,则直接从j+1继续寻找可以走完一圈的起始节点即可“,道理显而易见,只是很难意识到;这种优化可以省去非常多的时间,但写代码时有点陷阱,如果当前节点走不通了,则需要先判断一下该节点是不是已经从尾到头折返遍历的节点,这种情况不用跳跃更新起始节点,不然会陷入死循环;


136、只出现一次的数字(简单)

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

输入: [2,2,1]
输出: 1

输入: [4,1,2,1,2]
输出: 4

class Solution {
public:
    int singleNumber(vector<int>& nums){
        int result = 0;
        for(auto i : nums){
            result = result ^ i; // 异或操作符^
        }
        return result;
    }
};

思路:很简单的题,利用“一个数异或同一个数两次,原数不变”的规律即可;


138、复制带随机指针的链表(中等)

给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的 深拷贝。
我们用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。

提示:
-10000 <= Node.val <= 10000
Node.random 为空(null)或指向链表中的节点。节点数目不超过 1000 。

class Solution {
public:
    Node* copyRandomList(Node* head) {
        if(head==nullptr) return nullptr;
        CopyList(head); // 在链表自身中复制出一个新链表,每个新节点连接在原节点后
        CopyRandom(head); // 遍历链表,对新链表的random指针进行赋值
        Node * head2 = SplitList(head); // 分离两个链表,返回新链表头指针
        return head2;
    }
    void CopyList(Node* head){
        while(head!=nullptr){
            Node * node = new Node(head->val);
            node->next = head->next;
            head->next = node;
            head = node->next;
        }
    }
    void CopyRandom(Node* head){
        while(head!=nullptr){
            if(head->random!=nullptr){
                head->next->random = head->random->next;
            }
            head = head->next->next;
        }
    }
    Node * SplitList(Node* head){
        Node * head2 = head->next;
        while(head->next->next!=nullptr){
            Node * node = head->next->next;
            head->next->next = head->next->next->next;
            head->next = node;
            head = head->next;
        }
        head->next = nullptr;
        return head2;
    }
};

思路:剑指offer中的原题,复杂链表的复制,掌握思路后难度不大,链表复制、指针赋值和链表分离三个功能模块要细心处理;


139、单次拆分(中等)

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。

输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。

输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。

输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        if(s.empty()||wordDict.empty()) return false;
        set<string> dict(wordDict.begin(), wordDict.end()); // 将字典转换为哈希表,方便查找
        int size = s.size(); // 字符串长度
        vector<int> mem; // 记忆数组,用于存放可以被分割的某一段从头开始的字符串的结束下标
        mem.reserve(s.size()+1); // 先提前预留出足够的空间,提高效率 
        mem.push_back(-1); // 默认“string首元素之前的子串“是可以被分割的
        for(int i=0; i<size; ++i){ // 遍历string中的字符
            for(int j=0; j<mem.size(); ++j){ // 遍历mem记忆数组中的各个下标
                // 在哈希表中查找如下子串:起始于之前几个可以被分割的子串结束下标mem[j]之后,结束于当前遍历下标i
                // 如果查找到,则说明直到当前遍历节点i的子串部分都可以被分割,将下标i放入mem记忆数组
                if( dict.find( s.substr(mem[j]+1,i-mem[j]) )!=dict.end() ){
                    mem.push_back(i); // 压入新的可以被分割的子串的结束下标
                    break; // 以当前下标结束的子串已经可以被分割,不用继续遍历记忆数组,退出小循环;
                }
            }
        } // 如果最后一个被压入的可以被分割的子串结束下标正好是size-1,则说明整个数组都可以被分割;
        return ( mem.back()==(size-1) ); 
    }
};

思路:这道题稍微有点绕,一开始我想的是利用双指针left和right指定一段子串,遍历一次数组,如果查找到当前数组就更新left,查找再之后的子串,直到最后,如果最后一个子串也可以匹配,就说明匹配,但没考虑到之前的匹配序列如果不是最优匹配方式的问题,例子如"aaaaa",字典为{“aa”,“aaa”};正确结果是可以被分割,但按之前的思路只会识别两次aa,然后最后一个字符a无法被识别,返回false;故这道题在判断子串时,需要考虑到之前所有的可以被分割的序列,然后从每个之前可以被分割的序列之后作为新的子串来判断;具体步骤在代码注释,利用了一个vector来辅助存储之前可以被分割的子串们的结束下标,和当前遍历字符的下标组合成一个子串进行判断。如果新的子串也在字典中,则说明直到目前的子串都是可以被分割的。有点动态规划的意思;


140、单词拆分II(困难)

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中。返回所有这些可能的句子。

说明:
分隔时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。

输入:
s = “catsanddog”
wordDict = [“cat”, “cats”, “and”, “sand”, “dog”]
输出:
“cats and dog”,
“cat sand dog”

输入:
s = “pineapplepenapple”
wordDict = [“apple”, “pen”, “applepen”, “pine”, “pineapple”]
输出:
“pine apple pen apple”,
“pineapple pen apple”,
“pine applepen apple”
解释: 注意你可以重复使用字典中的单词。

输入:
s = “catsandog”
wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出:
[]

class Solution {
public:
    vector<string> wordBreak(string s, vector<string>& wordDict) {
        unordered_map<string, vector<string> > m; // map容器,存放一段字符串和其子串组合的映射,用于防止重复子问题的出现
        return helper(m, wordDict, s);
    }
    vector<string> helper(unordered_map<string,vector<string> >& m, vector<string>& wordDict, string s){
        if(m.count(s)) return m[s]; // 如果m中已经有这个子串组合了,则不用再重新找这段组合了,直接返回m中的现成组合
        if(s.empty()) return {""}; // 如果string已经为空了,则没有新的子串组合了,直接返回空串
        vector<string> res; // 存储当前的子串和其后面的子串组合 的组合
        for(auto word : wordDict){ // 遍历字典中的string
            if(s.substr(0,word.size())!=word) continue; // 该string不是s中的头部,直接continue
            else{ // 当前遍历的字典字段是s中的头部
                vector<string> tmp = helper(m, wordDict, s.substr(word.size())); // 如果是其头部,则递归搜索s的其余部分
                // 返回之后的子串组合tmp
                for(auto str : tmp){ // 遍历递归返回的各子串组合 
                    res.push_back( word+(str.empty()? "" : " "+str) ); // 当前的子串加上之后的各子串组合
                }
            }
        } 
        m[s]=res; // 利用下标操作在容器m中加入当前的pair(s,res);
        return res; // 返回当前的子串组合
    }
};

思路:该题与上一题在内容上十分相似,但问题略有不同,上一题是求有没有对应的分割方法,这一题是如果有对应的分割方法,则输出全部的分割方式;显然这一题使用深度优先搜索更合适(如递归),上一题的方法也可以拿来利用,但是改写比较复杂,不做尝试;上述代码采用递归的方式,其方法思路与上一题截然相反(上一题是在字符串中遍历,寻找存在于字典中的子串,然后继续遍历;这一题是在字典中遍历,寻找字典中是否存在一个与当前字符串头部相等的一个字词,然后继续递归处理后面的子串,这样可以保证考虑到所有的分割方式),各种分割方式的结果是从后往前进行存储的,具体思路见代码注释;另外一点,是采用了一个当前字符串与分割方式的哈希表,来存储已经处理过的子串和对应的分割结果,避免了重复子问题的出现,属于一种很好的优化方式;代码整体比较绕,这道题还是有一定的难度的;


141、环形链表(简单)

给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

class Solution {
public:
    bool hasCycle(ListNode *head) {
        if(head==nullptr) return false;
        ListNode * slow = head; // 慢指针
        ListNode * fast = head; // 快指针
        do{
            if( !slow || !slow->next ) return false; // 只要慢指针或快指针的下一步出现null,就没有环
            if( !fast || !fast->next || !fast->next->next ) return false;
            slow = slow->next; // 慢指针移动
            fast = fast->next->next; // 快指针移动
        }while(slow!=fast); // 两者相遇,则出现环
        return true;
    }
};

思路:简单的快慢指针应用,剑指offer原题;


147、LRU缓存机制(LRU页面置换算法,中等)

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥已经存在,则变更其数据值;如果密钥不存在,则插入该组「密钥/数据值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
你是否可以在 O(1) 时间复杂度内完成这两种操作?

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4

class LRUCache {
private:
    int cap; // 以下两者组成哈希链表,实现插入删除以及查找的O(1)操作
    list<pair<int,int>> cache; // 双向链表cache,元素为pair
    unordered_map<int, list<pair<int,int>>::iterator> map; // 哈希表map,用于在cache链表中快速查找
public:
    LRUCache(int capacity) { // 设定页面缓存的最大大小
        cap = capacity;
    }
    void put(int key, int value) { // 在cache中插入新页面
        auto it = map.find(key); // 在哈希表中获取指向cache迭代器的迭代器
        if(it==map.end()){ // 如果map中没有这个键值,则添加到链表首位
            if(cache.size()==cap){ // 添加前链表已满
                auto lastPair = cache.back(); // 获取最近最少被使用LRU的pair
                int lastKey = lastPair.first; // 获取这个最近最少被使用pair的键值key
                map.erase(lastKey); // 在map中删除这个键值对
                cache.pop_back(); // 在cache中删除这个pair
            } // 之后添加新的pair到链表头部,顺便更新map
            cache.push_front(pair<int,int>(key,value)); // 添加到cache头部
            map[key] = cache.begin(); // 添加到map
        }
        else{ // 添加的key值已经存在,则更想你这个键值对
            cache.erase(map[key]); // 在cache中删除指定的元素(巧妙利用迭代器删除链表元素)
            cache.push_front(pair<int,int>(key,value)); // 将新pair插入到cache头部
            map[key] = cache.begin(); // 连接到map并更新map
        }
    }
    int get(int key) { // 获取一个页面
        auto it = map.find(key);
        if(it==map.end()) return -1; // 查找不到,返回-1
        else{ // 查找到了
            pair<int,int> tmp = *map[key]; // 将该pair保存副本tmp
            cache.erase(map[key]); // 在cache中删除该pair并移到头部
            cache.push_front(tmp); // 在cache中将这个pair移到头部
            map[key] = cache.begin(); // 在map中更新这个pair的位置
            return tmp.second; // 返回查找的值
        }
    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

思路:LRU是最近最少被使用页面置换算法,是Linux系统、数据库查询中常用的页面置换算法,其实现需要有O(1)的插入、删除和查询操作,这个速度可以利用灵活组合各类数据结构做到:即哈希链表,在C++中的实现是:list> cache; 表示双向链表(页面的链表,头部存放刚使用的页面,尾部存放最长时间没被使用的页面,每个页面是一个pair,存放key和value),然后再建立一个unordered_map>::iterator> map; 用来当做每个页面的key和链表中相关页面的索引的映射关系(用迭代器来表示页面索引,这样就可以直接利用迭代器对页面进行增删),具体的使用方式见注释;

C++ : 力扣_Top(124-147)_第1张图片


你可能感兴趣的:(C++ : 力扣_Top(124-147))