目录
图论
51. 岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
解法一
深度优先搜索:对于矩阵中每一个‘1’点都将其看作三叉树(上下左右,其中一个为根生长方向,三个为子树方向)的节点,对每个为‘1’的节点进行深度优先搜索,搜索后就将其置为0,避免重复搜索。所寻找的岛屿则看作为一个全是‘1’的连通分量(连通树)。深度优先搜索了几次则代表有多少个岛屿。
class Solution {
private:
void dfs(vector<vector<char>>& grid, int r, int c) {
int nr = grid.size();
int nc = grid[0].size();
grid[r][c] = '0';
if (r + 1 < nr && grid[r+1][c] == '1') dfs(grid, r + 1, c);
if (r - 1 >= 0 && grid[r-1][c] == '1') dfs(grid, r - 1, c);
if (c + 1 < nc && grid[r][c+1] == '1') dfs(grid, r, c + 1);
if (c - 1 >= 0 && grid[r][c-1] == '1') dfs(grid, r, c - 1);
}
public:
int numIslands(vector<vector<char>>& grid) {
int nr = grid.size();
if (!nr) return 0;
int nc = grid[0].size();
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;
dfs(grid, r, c);
}
}
}
return num_islands;
}
};
class Solution {
public int numIslands(char[][] grid) {
int m = grid.length;
int n = grid[0].length;
int num = 0;
for(int i = 0; i < m ; i++){
for(int j = 0; j < n; j++){
if(grid[i][j] != '0'){
dfs(grid,i,j);
num++;
}
}
}
return num;
}
public void dfs(char[][] grid, int i, int j){
grid[i][j] = '0';
if((i-1) >= 0 && grid[i-1][j] == '1') dfs(grid,i-1,j);
if((j-1) >= 0 && grid[i][j-1] == '1') dfs(grid,i,j-1);
if(i+1 < grid.length && grid[i+1][j] == '1') dfs(grid,i+1,j);
if(j+1 < grid[0].length && grid[i][j+1] == '1') dfs(grid,i,j+1);
}
}
52. 腐烂的橘子
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
解法一
类似于上一题,以每个腐烂橘子为起点,进行广度优先搜索/深度优先搜索,确定每个橘子被腐蚀到的最短时间,之后再取全局最短时间即可。为了进行多个腐烂橘子一同开始向四周腐蚀,可采用一个队列按顺序将同一批次腐烂的橘子入队,这样就能保证全局最短时间一定是最后一个新鲜橘子被腐蚀的时间,例如全局最短时间如1111112222223333334444…。需注意,由于有的橘子可能永远不会被腐蚀,同时也需记录新鲜橘子剩余数量。
class Solution {
int cnt;
int dis[10][10];
int dir_x[4]={0, 1, 0, -1};
int dir_y[4]={1, 0, -1, 0};
public:
int orangesRotting(vector<vector<int>>& grid) {
queue<pair<int,int> >Q;
memset(dis, -1, sizeof(dis));
cnt = 0;
int n=(int)grid.size(), m=(int)grid[0].size(), ans = 0;
for (int i = 0; i < n; ++i){
for (int j = 0; j < m; ++j){
if (grid[i][j] == 2){
Q.push(make_pair(i, j));
dis[i][j] = 0;
}
else if (grid[i][j] == 1) cnt += 1;
}
}
while (!Q.empty()){
pair<int,int> x = Q.front();Q.pop();
for (int i = 0; i < 4; ++i){
int tx = x.first + dir_x[i];
int ty = x.second + dir_y[i];
if (tx < 0|| tx >= n || ty < 0|| ty >= m|| ~dis[tx][ty] || !grid[tx][ty]) continue;
dis[tx][ty] = dis[x.first][x.second] + 1;
Q.push(make_pair(tx, ty));
if (grid[tx][ty] == 1){
cnt -= 1;
ans = dis[tx][ty];
if (!cnt) break;
}
}
}
return cnt ? -1 : ans;
}
};
53. 课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
解法一
可以将问题转换为图论中寻找环的问题。其中两门课之间的依赖关系可以看作是图的有向边,每门课则为节点。可以通过深度优先搜索/广度优先搜索递归遍历节点,如果遍历过程中重新遍历到之前遍历过的节点,则表明存在环,无法完成课程;否则直至遍历完所有节点均未出现环,则表明可以完成课程。
需要注意以下几点:
- 先用某种数据结构,保存每节课需要提前完成的其他课(例如二维数组,嵌套链表)。然后依次遍历每节课,递归搜索每节课所需要提前完成的其他课,用visited数组记录哪些课已经被依赖了,防止循环依赖。
- 单纯的用bool类型的visited数组记录节点有没有被访问过是有漏洞的,因为本题暗藏了一个条件就是图是有向图,也就是说可能存在一个节点被多个其他节点指向被访问过,但是该节点并没有路径指回去,也就不构成环,但是它确实已经被访问过多次。
- 因此一个节点应该有三个状态,未被访问过,处于当前搜索过程中,处于其他已经搜索完的搜索过程中。
- 如果处于当前搜索过程中的节点再次被搜索到,才表明出现了环。
- 如果处于其他已经搜索完成的节点再次被搜索到,说明无需再继续往下搜索了,因为之前已经递归搜索过该节点,该节点能够到达的其他节点肯定也已经被搜索过,但是偏偏有另外的节点能够再次到达它,说它肯定不会再构成环了。假如构成环,那么到达它的节点应该之前就已经被递归搜索过了,而不是处于当前搜索过程中的节点再次搜索到它。
class Solution {
private:
vector<vector<int>> edges;
vector<int> visited;
bool valid = true;
public:
void dfs(int u) {
visited[u] = 1;//表明当前节点已被遍历到
for (int v: edges[u]) {
if (visited[v] == 0) {
dfs(v);
if (!valid) {
return;
}
}
else if (visited[v] == 1) {
valid = false;
return;
}
}
visited[u] = 2;//为了避免之前遍历过的地方在以另一个节点为起点时又重复遍历,浪费计算时间
}
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
edges.resize(numCourses);
visited.resize(numCourses);
for (const auto& info: prerequisites) {
edges[info[1]].push_back(info[0]);
}
//依次以每一个节点为起点进行深度优先搜索
for (int i = 0; i < numCourses && valid; ++i) {
if (!visited[i]) {//如果该起点未被dfs搜索过
dfs(i);
}
}
return valid;
}
};
class Solution {
boolean result = true;
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> course = new ArrayList<>();
int[] visited = new int[numCourses];
//visited有三种状态
//0未搜索到的
//1是正在搜索中的(当前轮次)
//2是已经搜索完的(不是当前轮次)
//建立每个课需要提前学习那些课的链表
for(int i = 0; i < numCourses; i++){
course.add(new ArrayList<Integer>());
}
int len = prerequisites.length;
for(int i = 0; i < len; i++){
course.get(prerequisites[i][0]).add(prerequisites[i][1]);
}
//遍历每个课对应依赖其他课的链表
for(int i = 0; i < numCourses && result; i++){
if(visited[i] == 0 ){
visited[i] = 1;
dfs(i,visited,course);
}
}
return result;
}
public void dfs(int index, int[] visited, List<List<Integer>> course){
List<Integer> list = course.get(index);
int len = list.size() ;
for(int i : list){
if(visited[i] == 0){
visited[i] = 1;
dfs(i,visited,course);
if(!result) return;
}
if(visited[i] == 1){
result = false;
return ;
}
}
//说明当前节点已经递归搜索完了
visited[index] = 2;
}
}
54. 实现 Trie (前缀树)
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
解法一
根据前缀树的特点,每个节点可以看作为一个字母,从树的根节点向下遍历则得到的字母序列组为字符串。由于小写字母共26个,则每个节点有26种可能,可以用一个数组或者哈希表来进行映射下一个字母树节点在哪儿。此外,还需要一个变量来辅助判断从根节点到当前节点遍历字母组成的字符串是不是真正存储的字符串(有可能只是字符串的子串)。
class Trie {
private:
vector<Trie*> children;
bool isend;
public:
Trie() : children(26),isend(false)
{}
void insert(string word) {
Trie* node = this;
for(char si : word)
{
si = si - 'a';
if(node->children[si] == nullptr)
{
node->children[si] = new Trie();
}
node = node->children[si];
}
node->isend = true;
}
bool search(string word) {
Trie* node = this;
for(char si : word)
{
si = si-'a';
if(node->children[si] == nullptr) return false;
node = node->children[si];
}
if(node->isend == false) return false;
else return true;
}
bool startsWith(string prefix) {
Trie* node = this;
for(char si : prefix)
{
si = si-'a';
if(node->children[si] == nullptr) return false;
node = node->children[si];
}
return true;
}
};
/**
* Your Trie object will be instantiated and called as such:
* Trie* obj = new Trie();
* obj->insert(word);
* bool param_2 = obj->search(word);
* bool param_3 = obj->startsWith(prefix);
*/
回溯
55. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
解法一
递归:由于数组长度不一,若想通过多层for循环依次列出所有解不现实。因此只能尝试递归。全排列相当于给n个格子里面填充元素。我们可以先确定第一个格子的值(共n种选择),之后再在剩余的n-1个值中选定第二个格子的值(共n-1中选择),依次如此直至n个格子填充满。我们可以将格子分成两个部分考虑,前一部分是已经确定的格子,后一部分是待排列的格子,因此后续我们只需要对后半部分再至执行全排列,依次递归。重点在于,每层递归只选择一个元素,被选过的元素不会再选了。
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> result;
arrange(result,nums,0,nums.size());
return result;
}
void arrange(vector<vector<int>> &result,vector<int> &nums,int first,int len)
{
if(first == len)
{
result.push_back(nums);
return;
}
for(int i = first; i < len ; ++i)
{
swap(nums[i],nums[first]);
arrange(result,nums,first + 1,len);
swap(nums[i],nums[first]);
}
}
};
class Solution {
boolean[] visited;
public List<List<Integer>> permute(int[] nums) {
visited = new boolean[nums.length];
List<List<Integer>> result = new ArrayList<>();
List<Integer> list = new ArrayList<>();
arrange(nums,result,0,list);
return result;
}
public void arrange(int[] nums, List<List<Integer>> result, int first, List<Integer> list){
if(first == nums.length){
result.add(new ArrayList<Integer>(list));
return;
}
for(int i = 0; i < nums.length; i++){
if(visited[i]) continue;
list.add(nums[i]);
visited[i] = true;
arrange(nums,result,first+1,list);
visited[i] = false;
list.remove(first);
}
}
}
56. 子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的
子集
(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集
解法一
按照数组顺序进行递归,每次递归选中一位数,再选下一位,下一位必须在第一位数的后面,这样就可以避免重复出现集合,例【1,2,3,4,5】,第一位选了2,则后续1,2就不可以选了,若第二位选了4,则后续1,2,3,4就不可以选了。由于每层递归可选的元素是多个,因此需要用一个循环考虑后续待选数的全部情况,并在递归返回时及时回溯。
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
vector<int> tmp;
result.push_back({});
subset(result,nums,tmp,0,nums.size());
return result;
}
void subset(vector<vector<int>> &result,vector<int> &nums,vector<int> &tmp,int first,int len)
{
if(first == len)
{
return;
}
for(int i = first; i < len ; ++i)
{
tmp.push_back(nums[i]);
result.push_back(tmp);
subset(result,nums,tmp,i+1,len);
tmp.pop_back();
}
}
};
解法二
借用二进制数的思想,1表示选取该位数字,0表示不选取该位数字。这样n个元素的数组,每个元素位置上有两种可能,选与不选,即可表示所有情况。例如n=3,则子集的情况有000,001,010,011,100,101,110,111。采用递归的方法就是每层递归产生两次递归,一次选取该层索引的元素,一次为不选取该层索引的元素,直接跳到下一个索引处。
class Solution {
public:
vector<int> t;
vector<vector<int>> ans;
void dfs(int cur, vector<int>& nums) {
if (cur == nums.size()) {
ans.push_back(t);
return;
}
t.push_back(nums[cur]);
dfs(cur + 1, nums);
t.pop_back();
dfs(cur + 1, nums);
}
vector<vector<int>> subsets(vector<int>& nums) {
dfs(0, nums);
return ans;
}
};
57. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
解法一
主体思想就是解析字符串,然后从每个数字对应的字符串中依次取一个字符出来进行组合。可采用递归的方法动态构建n层递归,每层选出对应数字字符串中的一个字符。
unordered_map<char, string> phoneMap{
{'2', "abc"},
{'3', "def"},
{'4', "ghi"},
{'5', "jkl"},
{'6', "mno"},
{'7', "pqrs"},
{'8', "tuv"},
{'9', "wxyz"}
};
class Solution {
public:
vector<string> letterCombinations(string digits) {
vector<string> result;
if(digits.empty()) return result;
string tmp;
sarrange(result,digits,tmp,0);
return result;
}
void sarrange(vector<string> &result,string &digits,string &tmp,int index)
{
if(index == digits.length())
{
result.push_back(tmp);
}
else
{
string s = phoneMap[digits[index]];
for(char c : s)
{
tmp.push_back(c);
sarrange(result,digits,tmp,index+1);
tmp.pop_back();
}
}
}
};
58. 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
解法一
由于每个数可以重复使用,则每次递归有两条途径,一个是继续使用当前的数,另一个用下一个数,若重复使用当前数,记得回溯。继续使用当前数的前提是累加和小于target或者target剩余值大于零。对于整体来说,如果超出了target要求范围则返回,等于target则需保存组合,此外,若所有的数都用完了,没有下一个元素了则也需要返回。
class Solution {
public:
void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int>& combine, int idx) {
if (idx == candidates.size()) {
return;
}
if (target == 0) {
ans.emplace_back(combine);
return;
}
// 直接跳过
dfs(candidates, target, ans, combine, idx + 1);
// 选择当前数
if (target - candidates[idx] >= 0) {
combine.emplace_back(candidates[idx]);
dfs(candidates, target - candidates[idx], ans, combine, idx);
combine.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> combine;
dfs(candidates, target, ans, combine, 0);
return ans;
}
};
解法二
方法二也是递归穷举,但是该方法效率更高。其思想为每层递归依次调用当前及其后面所有待选参数,也就把重复选值包含进去了。
class Solution {
public:
vector<int> cur;
void DFS(int begin,int sum,vector<int>& candidates,int target,vector<vector<int>> &res){
if (sum==target)
{
res.push_back(cur);
return;
}
if (sum>target)
{
return;
}
for (int i = begin; i < candidates.size(); i++)
{
cur.push_back(candidates[i]);
DFS(i,sum+candidates[i],candidates,target,res);
cur.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> res;
DFS(0,0,candidates,target,res);
return res;
}
};
59. 括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
解法一
暴力破解:递归列出所有可能情况。每层递归产生两种递归调用,一个是继续添加‘ ( ’,一个是继续添加‘ ) ’,递归终止条件为最终组合的字符串长度与2n相同。但是在终止条件中,需要判断该组合是否满足有效括号组合的要求,因此需要调用一个函数判断括号组合是否有效,即顺序遍历字符串时,正括号的数量必须时刻大于等于反括号的数量,但不能超过n,且最终两种括号总数相等,若不满足即返回false。
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> result;
string s;
dfs(result,s,n*2);
return result;
}
void dfs(vector<string> &result,string &s,int n)
{
if(n == 0)
{
if(valid(s))
{
result.push_back(s);
}
return;
}
s.push_back('(');
dfs(result,s,n-1);
s.pop_back();
s.push_back(')');
dfs(result,s,n-1);
s.pop_back();
}
bool valid(const string& str) {
int balance = 0;
for (char c : str) {
if (c == '(') {
++balance;
} else {
--balance;
}
if (balance < 0) {
return false;
}
}
return balance == 0;
}
};
解法二
解法一中有很多无效的组合被递归生成,且还需要花费时间进行筛查。解法二采用在递归过程中避免无效组合生成(例如“)))(((”)。将括号组合有效的特性引入递归过程中,采用两个变量记录分别正括号与反括号的数量,保证当正括号数量小于n时,可以先调用递归继续存入正括号;当反括号数量小于正括号数量时,才能调用递归加入反括号。由于有的时候既可以加正括号,也可以加反括号,因此一层递归应该最多尝试调用两种递归,且需要回溯。递归终止条件即为最终括号组合的长度等于2n,才将
当前组合放入最终结果中。
class Solution {
void backtrack(vector<string>& ans, string& cur, int open, int close, int n) {
if (cur.size() == n * 2) {
ans.push_back(cur);
return;
}
if (open < n) {
cur.push_back('(');
backtrack(ans, cur, open + 1, close, n);
cur.pop_back();
}
if (close < open) {
cur.push_back(')');
backtrack(ans, cur, open, close + 1, n);
cur.pop_back();
}
}
public:
vector<string> generateParenthesis(int n) {
vector<string> result;
string current;
backtrack(result, current, 0, 0, n);
return result;
}
};
60. 单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
解法一
采用深度优先搜索的方法,以每个网格点为搜索起点,依次向上下左右开始搜索(注意边界)。此外,由于网格中的元素不能重复使用,因此还需要一个数组标记已经使用过的网格,当递归返回时,使用标记需要回溯。
class Solution {
vector<vector<int>> copyboard;
public:
bool exist(vector<vector<char>>& board, string word) {
int row = board.size();
int col = board[0].size();
copyboard = vector<vector<int>>(row,vector<int>(col, 0));
for(int i = 0; i < row; ++i)
{
for(int j = 0; j < col; ++j)
{
if(dfs(board,word,i,j,0)) return true;
}
}
return false;
}
bool dfs(vector<vector<char>>& board, string &word, int row, int col, int index)
{
// if(index == word.size()) return true;
if(word[index] != board[row][col]) return false;
if(index == word.size()-1) return true;
int nr = board.size();
int nc = board[0].size();
bool res = false;
copyboard[row][col] = 1;
if(row + 1 < nr && !copyboard[row+1][col]) res = res || dfs(board,word,row+1,col,index+1);
if(row - 1 >= 0 && !copyboard[row-1][col]) res = res || dfs(board,word,row-1,col,index+1);
if(col + 1 < nc && !copyboard[row][col+1]) res = res || dfs(board,word,row,col+1,index+1);
if(col - 1 >= 0 && !copyboard[row][col-1]) res = res || dfs(board,word,row,col-1,index+1);
copyboard[row][col] = 0;
return res;
}
};
61. 分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
解法一
回溯+记忆搜索:找的组合是每个元素均为回文串,通过递归,依次搜索所有可能的字符串。当s前一部分已经被分割为若干个回文串后,后续则只需要搜寻如何把后部分的字符串分割为若干个子回文串,是一个明显的递归过程。递归过程主要为一步一步的切割子串,切割出一个回文串后,下次就从这个回文串之后的部分再切割一个回文串出来,循环往复。每次选定一个切割点,切割点以前的字符串为已经分割好的无需再次考虑,之后需递归尝试切割后半部分每个可能的切割点,所以一层递归中可能需要尝试生成多个递归。例如“aacdef”,若“aa”已被切割除去了,则下层递归则需要尝试先切割出“c”,或“cd”,或“cde”,或“cdef”,判断每种切割的子串是不是回文串,如果不是则继续尝试下一种切割。由于后三种情况均不满足回文串,则下一层递归表明“aa” “c”已经被切割出去,现在要递归考虑“def”怎么切,依次类推。注意一层递归中某种切割方式成功后一定要回溯,因为要尝试下一种切割方法。
由于每次都需要判断s(i,j)是不是回文串,可能会有多次重复计算。因此采用一个n*n的数组,记录每个s(i,j)是不是回文串(例如“abcba”),如果s(i+1,j-1)是回文串(“bcb”),则再只需判断s[i]与s[j]是否相等。
class Solution {
private:
vector<vector<int>> f;
vector<vector<string>> ret;
vector<string> ans;
int n;
public:
void dfs(const string& s, int i) {
if (i == n) {
ret.push_back(ans);
return;
}
for (int j = i; j < n; ++j) {
if (isPalindrome(s, i, j) == 1) {
ans.push_back(s.substr(i, j - i + 1));
dfs(s, j + 1);
ans.pop_back();
}
}
}
// 记忆化搜索中,f[i][j] = 0 表示未搜索,1 表示是回文串,-1 表示不是回文串
int isPalindrome(const string& s, int i, int j) {
if (f[i][j]) {
return f[i][j];
}
if (i >= j) {//当i=8,j=9,则后面return下次调用i=9,j=8,i比j大的情况为回文串搜索过头了,需要及时终止
return f[i][j] = 1;
}
return f[i][j] = (s[i] == s[j] ? isPalindrome(s, i + 1, j - 1) : -1);
}
vector<vector<string>> partition(string s) {
n = s.size();
f.assign(n, vector<int>(n));
dfs(s, 0);
return ret;
}
};
62. N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
解法一
递归:从第一层第一个位置到最后一个位置开始放置皇后,递归的放置下一层皇后…至最后一层。当最后到达最后一层且放置成功,说明这种放置策略是有效的,则将其输出。每次放置新一层的皇后时,均需要按照皇后规则检测该层放置方法会不会与之前上层的皇后发生冲突,即列,斜左上,斜右上三个方向上有无其他皇后。
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> result;
vector<string> tmp(n,string(n,'.'));
vector<int> colArray(n,0);
dfs(result,tmp,colArray,n,0);
return result;
}
void dfs(vector<vector<string>> &result,vector<string> &tmp, vector<int> &colArray, int n, int row)
{
if(row == n)
{
result.push_back(tmp);
return;
}
for(int i = 0; i < n; ++i)
{
if(colArray[i]) continue;
if( (row == 0) || rule(tmp,i,n,row))
{
colArray[i] = 1;
tmp[row][i] = 'Q';
dfs(result,tmp,colArray,n,row+1);
tmp[row][i] = '.';
colArray[i] = 0;
}
}
}
bool rule(vector<string> &tmp,int i,int n,int row)
{
bool b1 = true;
for(int j = 1; j <= row; ++j)
{
if(i+j < n)
{
b1 = b1 && (tmp[row-j][i+j] == '.' ? true : false);
}
if(i-j >= 0)
{
b1 = b1 && (tmp[row-j][i-j] == '.' ? true : false);
}
if(b1 == false) return false;
}
return true;
}
};
二分查找
63. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
解法一
由于数组有序且需要时间复杂度为 O(log n) ,可以采用二分查找算法。
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int top = nums.size()-1,low = 0;
int mid;
while(low <= top)
{
mid = ((top - low) >> 1) + low ;
// mid = (top + low) / 2;
if(nums[mid] < target)
{
low = mid + 1;
}
else
{
top = mid - 1;
}
}
return low;
}
};
64. 搜索二维矩阵
给你一个满足下述两条属性的 m x n 整数矩阵:
每行中的整数从左到右按非严格递增顺序排列。
每行的第一个整数大于前一行的最后一个整数。
给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。
解法一
将二维数组想象成一个长一些的一维数组,依然使用上一题的二分查找法,只不过在比较中间元素值需要索引二维数组的值,直接将mid数值转换为第i行第j列的元素。
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
int top = m*n - 1,low = 0;
int mid;
int j = 0,i = 0;
while(low <= top)
{
mid = ((top - low) >> 1) + low ;
// mid = (top + low) / 2;
i = mid / n;
j = mid % n;
if(matrix[i][j] == target) return true;
if(matrix[i][j] < target)
{
low = mid + 1;
}
else
{
top = mid - 1;
}
}
return false;
}
};
65. 在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
解法一
该题目明显是考察当有多个重复元素出现时,二分查找法如何探测重复元素的边界。由二分查找的原理可知,如果target元素存在,那么必有至少依次mid所指元素等于target。后续待求解的问题则是,mid == target后,low和top应该怎么移动。当循环条件为low <= top时,合法区间为闭区间【low,top】。
当mid == target时,如果top向左移动,那么后续mid肯定也会向左移动,此时就是在尝试探测重复元素的左边界。由于 mid最终可能会移动到比target小的元素上,且该过程一定是top向左移才导致mid也向左移至小元素上,则我们当mid == target时需保存一下上次mid所指的位置,当不满足条件的时候保存的上次mid的地方就是第一个target元素出现在最左侧的位置。
同理, 当mid == target时,如果low向右移动,那么后续mid肯定也会向右移动,此时就是在尝试探测重复元素的右边界。由于 mid最终可能会移动到比target大的元素上,且该过程一定是top向左移才导致mid也向右移至大元素上,则我们当mid == target时需保存一下上次mid所指的位置,当不满足条件的时候保存的上次mid的地方就是第一个target元素出现在最左右侧的位置。
但是由于上述两种情况,保存mid结果时出现的条件不一致,因此造成代码无法有效复用。通过找寻规律方法,可以发现无非就是当mid == target时,保存结果的代码是写在“low = mid + 1”旁边还是“top = mid - 1”旁边。这个可以通过nums[mid] > target还是nums[mid] >= target来调控。由于代码是固定的,如果将结果保存总是写在“top = mid - 1”的旁边,当条件为nums[mid] > target时,找到target后mid会右移(low = mid + 1),直到移到大于target的元素上(此时,low = mid = top,下一次top-1就退出循环了),则执行一次ans = mid,ans每次指向的为最右边的第一个重复元素的下一个元素位置。当条件为nums[mid] >= target时,如果将结果保存还是写在“top = mid - 1”的旁边,找到target后mid会左移,直到移到小于target的元素上(此时,low=mid=top,下一次执行low-1就要退出了),则上一次top = mid - 1后保存的ans = mid为最左边第一个重复元素的位置。
class Solution {
public:
int binarySearch(vector<int>& nums, int target, bool lower) {
int left = 0, right = (int)nums.size() - 1, ans = (int)nums.size();
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] > target || (lower && nums[mid] >= target)) {//通过lower来控制最终执行为大于还是大于等于
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}
vector<int> searchRange(vector<int>& nums, int target) {
int leftIdx = binarySearch(nums, target, true);//后续执行大于等于条件,ans返回为最左边第一个target重复元素
int rightIdx = binarySearch(nums, target, false) - 1;//执行大于条件,ans返回为最右边第一个target重复元素下一个元素位置
if (leftIdx <= rightIdx && nums[leftIdx] == target && nums[rightIdx] == target) {
//leftIdx <= rightIdx是为了保证当搜索数组为[]时,leftIdx为0,rightIdx为-1,索引无效
return vector<int>{leftIdx, rightIdx};
}
return vector<int>{-1, -1};
}
};
66. 搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
解法一
虽然数组不是完全有序,但是是前一部分有序和后一部分有序,且后一部分永远小于前一部分,可以利用这些特点修改二分查找法进行查找。
当mid 小于 nums[0]时,此时mid处于后半部分有序数组中。若target < nums[mid],则target掉入范围【nums[0],nums[mid-1]】,则top = mid-1;若target > nums[mid],则搜索范围有两个区间【nums[0],nums[mid-1]】和【nums[mid+1],nums[n-1]】,其一是当target <= nums[n-1]时,则low=mid+1,其二时当target>nums[n-1]时,则top=mid-1 ;
当mid大于等于nums[0]时,此时mid处于前半部分有序数组中,与上同理推导。
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
int top = n - 1,low = 0;
while(low <= top)
{
int mid = ((top - low) >> 1) + low ;
// mid = (top + low) / 2;
if(nums[0] <= nums[mid])
{
if(target < nums[mid] && target >= nums[0])
{
top = mid - 1;
}
else
{
low = mid + 1;
}
}
else
{
if(target > nums[mid] && target <= nums[n - 1])
{
low = mid + 1;
}
else
{
top = mid - 1;
}
}
if (nums[mid] == target) return mid;
}
return -1;
}
};
67. 寻找旋转排序数组中的最小值
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
解法一
采用二分查找法。当mid大于top时,则说明最小值还在mid右边,但是要注意细节例如当最后查找到“32”,top指向“2”,mid和low均指向“3”,此时mid也需要右移。因此当mid大于等于top时,low右移,即low= mid + 1;当mid小于top时,则说明最小值等于nums[mid]或者在mid左边,因此top需要左移。但由于最小值可能就在mid位置上,则top=mid,而不是top = mid - 1;
class Solution {
public:
int findMin(vector<int>& nums) {
int top = nums.size() - 1;
int low = 0;
int mid;
if(nums[low] <= nums[top]) return nums[low];
while(low < top )
{
mid = (top+low)/2;
if(nums[mid] >= nums[top])
{
low = mid + 1;
}
else
{
top = mid;
}
}
return nums[low];
}
};
68. 寻找两个正序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
解法一
根据两个数组的大小,我们可以确定若两个数组合并后中位数的位置k,因此可以将求取中位数的问题转换为求取两个数组中第k小的数。当总长度为奇数时,k = (m+n) / 2;当总长度为偶数时,需要寻找两个数取平均,分别为k = (m+n) / 2 和 (m+n+1) / 2。
由于时间复杂度限制为log(m+n)且两数组有序,因此需要采用二分查找法。每次比较两个数组的第k/2-1位置上的数,哪一个数组上的数小,则说明从数组0到k/2-1的所有数一定是全局最小的,可以将其排除(排除将近一半),后续只用寻找k = k - k/2 +1个数了,之后该数组只需从k/2作为首元素和另外一个数组继续查找下一个k/2-1上的数。循环往复,至k == 1,则返回两个数组剩余部分的首元素的最小值。
由于数组不断地在排除前面的元素,因此后续可能出现剩余数组元素个数小于k/2-1个,此时若还按照上述流程则数组会越界。因此每次排除的元素个数不能总为k/2-1,而应该是min(low1 + k/2 - 1, m - 1)。例如【1,2,3,4】,k = 6,最后只剩元素4未被排除,此时k/2 - 1 = 2,若后续该数组对比后需要排除元素,则应该只排除一个元素4,并且k只减一。
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int length = nums1.size()+nums2.size();
if(length % 2 == 1)
{
return getKelem(nums1,nums2,length/2 + 1);
}
else
{
return (getKelem(nums1,nums2,length/2) + getKelem(nums1,nums2,length/2 + 1))/2.0;
}
}
int getKelem(vector<int>& nums1, vector<int>& nums2,int k)
{
int m = nums1.size();
int n = nums2.size();
int low1 = 0;
int low2 = 0;
while(true)
{
if(low1 == m) return nums2[low2 + k -1];
if(low2 == n) return nums1[low1 + k -1];
if(k == 1) return min(nums1[low1], nums2[low2]);
int newindex1 = min(low1 + k/2 - 1, m - 1);
int newindex2 = min(low2 + k/2 - 1, n - 1);
if(nums1[newindex1] >= nums2[newindex2])
{
k -= newindex2 - low2 + 1;
low2 = newindex2 + 1;
}
else
{
k -= newindex1 - low1 + 1;
low1 = newindex1 + 1;
}
}
}
};
栈
69. 有效的括号
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
解法一
用一个栈存储左括号。遍历字符串,遇到左括号时则压栈,遇到右括号时则出栈。如果出栈的左括号与右括号不匹配,则字符串无效。如果栈中没有左括号而先遍历到右括号,则字符串无效。如果最后栈不为空,则说明有多余的左括号,则字符串无效。
class Solution {
public:
bool isValid(string s) {
vector<char> leftv;
for(char c : s)
{
if(c == '(' || c == '{' || c == '[')
{
leftv.push_back(c);
}
else
{
if(leftv.size() == 0) return false;
switch(c)
{
case ')':
{
if(leftv.back() == '(')
leftv.pop_back();
else
return false;
break;
}
case '}':
{
if(leftv.back() == '{')
leftv.pop_back();
else
return false;
break;
}
case ']':
{
if(leftv.back() == '[')
leftv.pop_back();
else
return false;
break;
}
}
}
}
if(leftv.size() == 0) return true;
return false;
}
};
//java
class Solution {
public boolean isValid(String s) {
LinkedList<Character> list = new LinkedList<>();
Map<Character,Character> pairs = new HashMap<Character,Character>();
pairs.put(')','(');
pairs.put(']', '[');
pairs.put('}', '{');
for(char c : s.toCharArray()){
if(pairs.containsKey(c)){
if( list.isEmpty() || pairs.get(c) != list.peek()){
return false;
}
list.pop();
}else{
list.push(c);
}
}
if( list.isEmpty() ) return true;
else return false;
}
}
70. 最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
MinStack() 初始化堆栈对象。
void push(int val) 将元素val推入堆栈。
void pop() 删除堆栈顶部的元素。
int top() 获取堆栈顶部的元素。
int getMin() 获取堆栈中的最小元素。
解法一
其他栈操作比较普通,可以轻易实现。其中有一个需要以常数时间复杂度获取栈中最小元素,因此需要在记录元素栈之外,额外添加一个记录每个元素入栈时对应的此时栈中最小元素的栈,与其分别入栈x_stack和min_stack。
class MinStack {
stack<int> x_stack;
stack<int> min_stack;
public:
MinStack() {
min_stack.push(INT_MAX);
}
void push(int x) {
x_stack.push(x);
min_stack.push(min(min_stack.top(), x));
}
void pop() {
x_stack.pop();
min_stack.pop();
}
int top() {
return x_stack.top();
}
int getMin() {
return min_stack.top();
}
};
71. 字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
解法一
涉及到正反括号对应的任务,可以采用栈。当没有遇到‘】’符号时,直接将字符入栈;如果遇到了,则开始陆续出栈至遇到‘【’,则中间出栈的字符组成需要重复k次的字符串,之后再继续出栈,得到‘【’之前的数字k,表示重复次数,将字符串重复拼接后再入栈,循环往复至字符串被遍历完。最后遍历整个栈输出最终字符串。需要注意的是,如果重复字数是1位数以上,例如’100’',则需要一次性读取三个字符为重复次数。
class Solution {
public:
string getDigits(string &s, size_t &ptr) {
string ret = "";
while (isdigit(s[ptr])) {
ret.push_back(s[ptr++]);
}
return ret;
}
string getString(vector <string> &v) {
string ret;
for (const auto &s: v) {
ret += s;
}
return ret;
}
string decodeString(string s) {
vector <string> stk;
size_t ptr = 0;
while (ptr < s.size()) {
char cur = s[ptr];
if (isdigit(cur)) {
// 获取一个数字并进栈
string digits = getDigits(s, ptr);
stk.push_back(digits);
} else if (isalpha(cur) || cur == '[') {
// 获取一个字母并进栈
stk.push_back(string(1, s[ptr++]));
} else {
++ptr;
vector <string> sub;
while (stk.back() != "[") {
sub.push_back(stk.back());
stk.pop_back();
}
reverse(sub.begin(), sub.end());
// 左括号出栈
stk.pop_back();
// 此时栈顶为当前 sub 对应的字符串应该出现的次数
int repTime = stoi(stk.back());
stk.pop_back();
string t, o = getString(sub);
// 构造字符串
while (repTime--) t += o;
// 将构造好的字符串入栈
stk.push_back(t);
}
}
return getString(stk);
}
};
解法二
递归:每次遇到字母就添加到输出字符串中,遇到数字则进行保存,遇到‘【’就递归调用向后搜索至‘】’,返回子串,之后将子串*数字扩展。由于后续可能还有字符没有遍历到,所以每次调用完递归函数返回之前,需要继续获取后面的字符串,将当前递归层的字符串与其后面的字符串相拼接,然后一起返回。
下面递归函数getString本质上每轮递归只从原始字符串中获取一个“字母”。
class Solution {
int ptr;
String src;
public String decodeString(String s) {
src = s;
ptr = 0;
return getString();
}
public String getString(){
if(ptr >= src.length() || src.charAt(ptr) == ']'){
return "";
}
StringBuilder sb = new StringBuilder();
String sub = null;
int num = 0;
if(Character.isDigit(src.charAt(ptr))){
num = getDigits();
}
if(src.charAt(ptr) == '['){
ptr++;
sub = getString();
ptr++;
while(num-- > 0){
sb.append(sub);
}
}else if(Character.isLetter(src.charAt(ptr))){
sb.append(src.charAt(ptr));
ptr++;
}
//由于后续可能还有字符没有遍历到,所以继续获取后面的字符串
return sb.append(getString()).toString();
}
public int getDigits(){
int end = ptr + 1;
while(Character.isDigit(src.charAt(end))){
end++;
}
int num = Integer.parseInt(src.substring(ptr,end));
ptr = end;
return num;
}
}
72. 每日温度
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
解法一
该问题可以转换为寻找下一个比自己大的元素位置。可以采用单调栈,栈中记录元素索引。如果当前第i个元素比栈顶元素小,则元素入栈,栈单调递减;否则,将栈顶元素出栈,并且表明当前第i个元素是比该出栈索引对应的元素大的第一个元素(从左到右),对应栈顶元素记录比自己大的元素的索引 i ,循环往复,至遇到比自己大的栈顶元素或栈为空,则该元素出栈。
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
vector<int> index(temperatures.size(),0);//记录下一个比自己大的元素下标
vector<int> stk;//单调栈
for(int i = 0;i < temperatures.size() ; ++i)
{
if( stk.empty() || temperatures[stk.back()] >= temperatures[i])
{
stk.push_back(i);
}
else
{
while(!stk.empty() && temperatures[stk.back()] < temperatures[i])
{//将比当前元素大的元素的索引出栈
index[stk.back()] = i;
stk.pop_back();
}
stk.push_back(i);//将该元素入栈,保证栈的单调性
}
}
for(int i = 0; i < index.size() ; ++i)//index元素值与索引i的差即为当前元素到下一个比自己大的元素的距离
{
if(index[i] != 0)
{
index[i] = index[i] - i;
}
}
return index;
}
};
73. 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
解法一
暴力解法:一次遍历每个柱子,每个柱子均向左和向右扩展,若遇到比它小的,则其以该柱子高度可扩展的矩形方法便可确定下来,时间复杂度为n*n。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
int ans = 0;
for (int mid = 0; mid < n; ++mid) {
// 枚举高
int height = heights[mid];
int left = mid, right = mid;
// 确定左右边界
while (left - 1 >= 0 && heights[left - 1] >= height) {
--left;
}
while (right + 1 < n && heights[right + 1] >= height) {
++right;
}
// 计算面积
ans = max(ans, (right - left + 1) * height);
}
return ans;
}
};
解法二
单调栈:与解法一的想法类似,固定矩形高度,寻找向两边可扩展的宽。但是可以通过单调栈减少寻找两边可扩展宽的计算。依次遍历每根柱子,尝试将其压栈,构建一个单调递增栈。
如果栈顶元素小于等于当前元素,则说明将栈中的元素和当前元素作为高搜寻的宽还有可能继续向右扩展,因此均无法确定以栈中元素为高的矩形大小,还需继续向右搜索;
如果栈顶元素大于当前元素,则说明以栈顶元素为高的矩形的右边界已经确定为当前元素的索引值,左边界也可确定,因为栈是单调的,左边界为栈顶下一个元素的索引值,即可算出以当前元素为高的矩形面积。
当有两个柱子高度相等时,有一个柱子最大面积计算会不对,但是不影响正确答案,因为另外一个柱子会计算对。例如【1,5,5,4】,当1,5(记录符号为a),5(记录符合为b)入栈。遇到4后,5b要出栈,由于栈是单调增的,因此认为5b大于5a,则以5b为高度的矩形为5*(4的索引值 - 5b的索引值) = 51,计算其实是错误的,应当为52。但是当5b出栈以后,4与5a比较,5a也需出栈,此时计算以5a为高的矩形则为5*(4的索引值 - 5a的索引值) = 5*2,即为正确答案。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
vector<int> left(n), right(n, n);
stack<int> mono_stack;
for (int i = 0; i < n; ++i) {
while (!mono_stack.empty() && heights[mono_stack.top()] >= heights[i]) {
right[mono_stack.top()] = i;
mono_stack.pop();
}
left[i] = (mono_stack.empty() ? -1 : mono_stack.top());
mono_stack.push(i);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
ans = max(ans, (right[i] - left[i] - 1) * heights[i]);
}
return ans;
}
};
堆
74. 数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
解法一
对数组建立大根堆数据结构,每次取出根元素,再调整堆结构,循环k-1次,则取出了k-1个最大值,之后再取出堆顶元素即为第k大的值。本题主要需要熟悉建堆和调整堆的过程。
class Solution {
public:
void maxHeapAdjust(vector<int>& nums,int i,int heapSize)
{
int left = i*2 + 1 , right = i*2 + 2;
int largest = i;
if(heapSize > left && nums[largest] < nums[left])//判断根、左谁更大
{
largest = left;
}
if(heapSize > right && nums[largest] < nums[right])//判断根、右谁更大
{
largest = right;
}
if(largest != i)//如果最大的不是根节点,那就要将大元素上移,根元素下移
{
swap(nums[i],nums[largest]);//子树根节点与左or右大节点交换
maxHeapAdjust(nums,largest,heapSize);//被换下去的节点可能也会继续往下换,因此递归调用
}
}
void buildMaxHeap(vector<int>& nums,int heapSize)
{
for(int i = heapSize / 2 - 1; i >= 0 ; --i)//最多有heapSize/2个非叶子节点
{
maxHeapAdjust(nums,i,heapSize);//每个节点需要维护自己的左右节点与自己(根)的大小关系
}
}
int findKthLargest(vector<int>& nums, int k) {
int heapSize = nums.size();
buildMaxHeap(nums , heapSize);
for(int i = 1 ; i < k ; ++i)
{
swap(nums[0],nums[heapSize - 1]);//循环抽出k-1次堆顶元素(即最大元素),并将其放在后面,将堆中最后一个元素(取出过的最大元素除外)放在顶端
--heapSize;
maxHeapAdjust(nums,0,heapSize);//顶端节点可能不满足大根堆的性质,因此需要重新将最大元素交换到堆的根节点上
}
return nums[0];
}
};
75. 前 K 个高频元素
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
解法一
本题可以转换为求取“频率数组中”前k个最大值,这样就可以使用上题中的大根堆数据结构了。首先可以用哈希表统计数字出现次数,之后将哈希表中的元素存入动态数组vector中,再对动态数组建堆,最后依次取k个堆顶元素即可。
class Solution {
public:
void maxHeapAdjust(vector<pair<int,int>>& nums,int i,int heapSize)
{
int left = i*2 + 1 , right = i*2 + 2;
int largest = i;
if(heapSize > left && nums[largest].second < nums[left].second)//判断根、左谁更大
{
largest = left;
}
if(heapSize > right && nums[largest].second < nums[right].second)//判断根、右谁更大
{
largest = right;
}
if(largest != i)//如果最大的不是根节点,那就要将大元素上移,根元素下移
{
swap(nums[i],nums[largest]);//子树根节点与左or右大节点交换
maxHeapAdjust(nums,largest,heapSize);//被换下去的节点可能也会继续往下换,因此递归调用
}
}
void buildMaxHeap(vector<pair<int,int>>& nums,int heapSize)
{
for(int i = heapSize / 2 - 1; i >= 0 ; --i)//最多有heapSize/2个非叶子节点
{
maxHeapAdjust(nums,i,heapSize);//每个节点需要维护自己的左右节点与自己(根)的大小关系
}
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> occurences;
for(auto v : nums)
{
occurences[v]++;
}
vector<pair<int,int>> ov;//第一个为数字,第二个为该数字出现次数
int i = 0;
for(auto& vp : occurences)
{
ov.emplace_back(vp);
++i;
}
int heapSize = ov.size();
vector<int> ret;
buildMaxHeap(ov , heapSize);
for(int i = 0 ; i < k ; ++i)
{
ret.push_back(ov[0].first);
swap(ov[0],ov[heapSize - 1]);//循环抽出k-1次堆顶元素(即最大元素),并将其放在后面,将堆中最后一个元素(取出过的最大元素除外)放在顶端
--heapSize;
maxHeapAdjust(ov,0,heapSize);//顶端节点可能不满足大根堆的性质,因此需要重新将最大元素交换到堆的根节点上
}
return ret;
}
};
76. 数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4] 的中位数是 3 。
例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder 类:
MedianFinder() 初始化 MedianFinder 对象。
void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。
解法一
可以维护两个优先队列(堆),一个队列记录大于中位数的数,一个队列记录小于等于中位数的数,两个队列长度之差不能超过1。若默认小队列长度总是大于等于大队列长度这样,中位数每次可取小队列的头元素(总长度奇数)或者两队列头元素的平均值(总长度偶数)。添加元素时注意,需要时刻保持两个队列长度之差小于1,如果不满足,则需要及时将一个队列的头元素搬移到另一个队列。
class MedianFinder {
public:
priority_queue<int, vector<int>, less<int>> queMin;//记录小于等于中位数的队列
priority_queue<int, vector<int>, greater<int>> queMax;//记录大于中位数的队列
MedianFinder() {}
void addNum(int num) {
if (queMin.empty() || num <= queMin.top()) {//默认小队列长度总是大于等于大队列长度
queMin.push(num);
if (queMax.size() + 1 < queMin.size()) {//维护两个队列长度,使其保持平衡,以至于每次中位数可从两个队列头部计算得出
queMax.push(queMin.top());//移动小于中位数队列中的最大值
queMin.pop();
}
} else {
queMax.push(num);
if (queMax.size() > queMin.size()) {//维护两个队列长度
queMin.push(queMax.top());
queMax.pop();
}
}
}
double findMedian() {
if (queMin.size() > queMax.size()) {
return queMin.top();
}
return (queMin.top() + queMax.top()) / 2.0;
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
贪心算法
77. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
解法一
暴力破解:每两个数之间做差取最大值。时间复杂度n*n
解法二
双指针:买卖股票我们希望再最低时买入,最高时卖出以获得最大利润。因此,我们可以用双指针,一个快,一个慢。并且需要记录股价最低时的索引和股价最高时的索引。当快指针遇到比历史股价更低的股价时,则需要记录,并且将快慢指针全部移到当前位置(因为旧的最低股价已经无法在后面的比较中适用了),再继续向后移动。由于一次遍历下来,可能有多个历史最低最高差价,因此需要保留最大值。例如【4,80,2,10,8】,则会有两次历史最低价最高价变化,即80-4,10-2,最终取80-4。由于只需遍历一次数组,时间复杂度位n。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int low = prices[0],high = low;
int i = 0;
int maxpro = 0;
for(int j = 1; j < prices.size(); ++j)
{
if(low > prices[j])
{
i = j;
low = prices[j];
high = prices[j];
}
if(high < prices[j])
{
high = prices[j];
}
if(high - low > maxpro)
{
maxpro = high - low;
}
}
return maxpro;
}
};
更简洁的写法如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int inf = INT_MAX;
int minprice = inf, maxprofit = 0;
for (int price: prices) {
maxprofit = max(maxprofit, price - minprice);
minprice = min(price, minprice);
}
return maxprofit;
}
};
78. 跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
解法一
每遍历到一个元素,就计算目前可以达到最远的距离,保存最远能到达的距离。如果可到达的最远距离大于等于数组长度,即表示可以到达最后一个下标;如果可到达的最大距离小于数组长度,且该最远距离之前的元素均已遍历,无法更新至更远的距离,则说明最后一个下标不可达。
class Solution {
public:
bool canJump(vector<int>& nums) {
int currentfar = 0;
int length = nums.size();
for(int i = 0; i < length ; ++i)
{
if(i <= currentfar)
{
currentfar = max(currentfar, i + nums[i]);
if(currentfar >= length-1) return true;
}
else
{
return false;
}
}
return false;
}
};
79. 跳跃游戏 II
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
解法一
题目乍一想可以用递归回溯计算出所有可能的结果,再取最小跳跃次数,但是时间复杂度较高。
延续上题的思路,我们可以得知跳一次可到达的最大位置,我们在固定次数跳跃中,尽可能跳到最大的位置。假设,我们可以顺序搜索上一起跳点到可达最大位置中的元素,获取增加一次跳跃后能到达的最大位置。例如,【4,1,1,4,1,1,1,1,1,1】,起跳点位序号0,最远可达序号4,此时起跳1次。如果起跳两次,接下来就需要比较起点1到4的元素中作为第二次起跳点,最远可达哪里。最后发现第二次起跳点如果设为序号3,两次跳跃最远可达序号7。如此往复,每次就是搜索固定跳跃次数中,最远能跳到哪里,如果最远能到最后一个元素,则该次数就是全局最小的。可自行画图理解。
class Solution {
public:
int jump(vector<int>& nums) {
int length = nums.size();
int currentfar = nums[0];
int count = 0,end = 0;
for(int i = 0 ; i < length - 1 ; ++i)
{
if(i <= currentfar)
{
currentfar = max(currentfar,i + nums[i]);
if(i == end)
{
end = currentfar;
++count;
}
}
}
return count;
}
};
80. 划分字母区间
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
解法一
由于每个字符只能出现在一个片段中,因此可以记录每个字符最后一次出现的索引。如果某一片段所有字母的索引都小于等于片段中所包含字母最大索引,则可以独立成为一个小片段;否则更新出更大的索引,继续往后寻找。例如“bcacbaeee”,从首元素开始遍历,b出现的最后一次在4,c出现的最后一次在3,a出现的最后一次在5,0~5之间仅有这三种字符,因此0-5可以成为一个小片段“bcacba”。
class Solution {
public:
vector<int> partitionLabels(string s) {
int last[26];
int length = s.size();
for(int i = 0; i < length; ++i)
{
last[s[i] - 'a'] = i;
}
vector<int> partition;
int start = 0, end = 0;
for(int i = 0; i < length; ++i)
{
end = max(end,last[s[i] - 'a']);
if(i == end)
{
partition.push_back(end - start + 1);
start = end + 1;
}
}
return partition;
}
};
81. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
解法一
动态规划:当爬到x层时,总方案个数应该为爬到x-1层的总方案个数+爬到x-2层的总方案个数。因此可以得出表达式(x) = f(x-1)+f(x-2),经过数学归纳,可得到初始条件,f(1) = 1 + 0 ,f(0) = 1。后续,f(2) = f(1)+f(0)…。每次计算都是三个数,新的前两个数继承旧的后两个数,计算得到结果(第三个数)。
class Solution {
public:
int climbStairs(int n) {
int fx1 = 0,fx2 = 0,fx = 1;
for(int i = 1; i <= n; ++i)
{
fx2 = fx1;
fx1 = fx;
fx = fx1 + fx2;
}
return fx;
}
};
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n; i++){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
}
82. 杨辉三角
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
解法一
动态规划:根据杨辉三角的特性,可以找出第n层与第n-1层每个元素之间的关系。即第n层结果V1,第n-1层结果V0,V1[x] = V0[x-1] + V0[x],当x-1或x超出V0的界限后,对应值为0即可。
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<int> v = {1};
vector<vector<int>> result;
result.emplace_back(v);
int left = 0, right = 0;
for(int i = 2; i <= numRows; ++i)
{
int lastlength = result[i-2].size();
vector<int> tmp;
for(int j = 0; j <= lastlength; ++j)
{
if(j == 0) left = 0;
else left = result[i-2][j-1];
if(j == lastlength) right = 0;
else right = result[i-2][j];
tmp.emplace_back( left + right );
}
result.emplace_back(tmp);
}
return result;
}
};
class Solution {
public List<List<Integer>> generate(int numRows) {
Integer[][] dp = new Integer[numRows][];
for(int i = 0; i < numRows; i++){
dp[i] = new Integer[i+1];
for(int j = 0; j <= i;j++){
if(j == 0 || j == i){
dp[i][j] = 1;
}else{
dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
}
}
}
List<List<Integer>> result = new ArrayList<>();
for(int i = 0; i < numRows; i++){
result.add(Arrays.asList(dp[i]));
}
return result;
}
}
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> ret = new ArrayList<List<Integer>>();
for (int i = 0; i < numRows; ++i) {
List<Integer> row = new ArrayList<Integer>();
for (int j = 0; j <= i; ++j) {
if (j == 0 || j == i) {
row.add(1);
} else {
row.add(ret.get(i - 1).get(j - 1) + ret.get(i - 1).get(j));
}
}
ret.add(row);
}
return ret;
}
}
83. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
解法一
动态规划:动态规划问题主要是要提炼出规律,将得到一个递推的公式,加上边界条件即可求解。假设偷到第 i 间房,此时有两种策略:
- 下手偷第 i 间房,那么之前肯定不会偷 第 i - 1 间房,偷的最大金额为前i - 2 间房的最大金额加上第 i 间房的金额,即dp【i - 2】 + nums[ i ];
- 不偷第 i 间房,偷的最大金额为前 i - 1间房的最大金额,即dp【i - 1】;
取两种策略最大值即可。可以得到如下公式:
dp【 i 】 = max(dp【i - 2】 + nums[ i ] , dp【i - 1】 ) 表示前 i 间房最高总金额;
边界条件为
dp【0】 = nums[ 0 ] 表示仅有一间房时;(因为数组从0开始,这样便于编写代码)
dp【1】 = max( nums[ 1 ] , nums[ 0 ] ) 表示仅有两间房时;
dp【 2 】 = max(dp【 0 】 + nums[ 2 ] , dp【 1 】 )
dp【 3 】 = max(dp【 1 】 + nums[ i ] , dp【 2 】 )
dp【 n - 1 】 = max(dp【 n - 3 】 + nums[ n - 1 ] , dp【 n -2 】 ) (数组最后一个元素为第n-1个)
…
class Solution {
public:
int rob(vector<int>& nums) {
int length = nums.size();
if(length == 1) return nums[0];
/*
int *dp = new int[length];//用数组记录偷到第i间的最大总金额
dp[0] = nums[0];
dp[1] = max(nums[1],nums[0]);
for(int i = 2; i < length; ++i)
{
dp[i] = max(dp[i-2] + nums[i],dp[i-1]);
}
return dp[length - 1];
*/
int dpi_2 = nums[0], dpi_1 = max(nums[0],nums[1]);
int dpi = dpi_1;//用滚动数组记录偷到第i间的最大总金额,dpi之和dpi-1、dpi-2有关
for(int i = 2; i < length; ++i)
{
dpi = max(dpi_2 + nums[i], dpi_1);
dpi_2 = dpi_1;
dpi_1 = dpi;
}
return dpi;
}
};
下面这个思路有些不一样,dp[i]是偷了第i家后最大收益
class Solution {
public int rob(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
int result = dp[0];
for(int i = 1; i < nums.length; i++){
if(i == 1){
dp[1] = Math.max(nums[0],nums[1]);
}else if(i == 2){
dp[2] = Math.max(nums[0]+nums[2],nums[1]);
}else{
dp[i] = Math.max(dp[i-2],dp[i-3]) + nums[i];
}
result = Math.max(result,dp[i]);
}
return result;
}
}
84. 完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
解法一
动态规划:
- 确定dp数组含义:dp[i]为组成数字 i 最少完全平方数的个数。
- 建立递推公式:dp【i】 = min(dp[ i - j ] + dp[ j ]其中 j 从1 到 i / 2 )。经过思考可以将其优化,由于数字i最后都会被分解为若干个完全平方数,因此在筛选最小值时,我们可以让 j 每次等于一个完全平方数(该完全平方数会小于等于 i ,保证i - j有定义),因此j的搜索次数可以减少。得到最终的递推公式:dp【i】 = min(dp[ i - j * j ] + dp[ j*j ]) = min(dp[ i - j * j ] + 1)其中j从1到根号 i 。
- 边界条件:dp[0] = 0; dp[1] = 1;
- 遍历顺序:从小到大;
原始版:
class Solution {
public:
int numSquares(int n) {
int dp[n+1];
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i <= n ; ++i)
{
int square = sqrt(i);
if(square * square == i)
{
dp[i] = 1;
continue;
}
dp[i] = INT_MAX;
for(int j = 1; j <= i/2; ++j)
{
dp[i] = min(dp[i] , dp[i-j] + dp[j]);
}
}
return dp[n];
}
};
优化版:
class Solution {
public:
int numSquares(int n) {
int dp[n+1];
dp[0] = 0;
for(int i = 1; i <= n ; ++i)
{
int minint = INT_MAX;
for(int j = 1; j*j <= i; ++j)
{
minint = min(minint , dp[i-j*j]);
}
dp[i] = minint + 1;
}
return dp[n];
}
};
85. 零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
解法一
动态规划:
- 确定dp数组含义:dp[i]为组成总金额为 i 最少硬币个数。
- 建立递推公式:dp【i】 = min(dp[ i - val ] + 1 ] 其中 val 可能为硬币数组中的任意一个元素 )。
- 边界条件:dp[0] = 0;
- 遍历顺序:从小到大;
由于题目中还可能存在无法用若干硬币(有限个or无限个)组成总金额 i 的情况,也就是无法通过递推公式获得一个比初始值更小的值,dp【i】的值将从一开始到最后都不发生变化。因此可以将dp数组每个元素初始化为一个比较大的值,若最后dp【i】依旧为那个值,则说明没有硬币方案可以实现总金额为 i 。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp( amount + 1 ,amount + 1);
dp[0] = 0;
for(int i = 1; i <= amount; ++i )
{
for(auto& val : coins)
{
if(i - val >= 0)
{
dp[i] = min(dp[i],dp[i - val] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
86. 单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
解法一
动态规划:
- 确定dp数组含义:dp[i]为s的前i个字符组成的字符串能否被拆分为若干个字典中出现的词。
- 建立递推公式:dp【i】 = dp[ j ] && check(s[ j… i-1 ]) 其中 j 为0 到 i - 1)。这里的 j 相当于把0 到 i -1的字符串分割成了两部分,每次判断前半部分是否能被划分成若干个字典中的词,并且后半部分字符串是否在字典中出现过。如果至少有一种情况dp【i】为真,则dp【i】就为true。
- 边界条件:dp[0] = true,因为递推公式中有dp【0】&&check(s[ 0… i-1 ])的情况,相当于自检本段字符串是否在字典中,因此要保证dp【0】为真才行;
- 遍历顺序:从字符串首元素遍历到最后;
未优化代码:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> set;//用于后续判断子串在不在字典中
for(auto str : wordDict)
{
set.insert(str);
}
int n = s.length();
bool dp[n+1];
dp[0] = true;
for(int i = 1; i <= n; ++i)
{
dp[i] = false;
for(int j = 0; j < i; ++j )
{
dp[i] = dp[i] || (dp[j] && set.count(s.substr( j , i - j)));
}
}
return dp[n];
}
};
优化后的代码:其中增加了记录字典中最长字符串的长度,当i - j大于该长度时,则后续check(s[ j… i-1 ])就没必要了,必定不会出现在字典里。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> set;
int maxlength = 0;
for(auto str : wordDict)
{
set.insert(str);
maxlength = max(maxlength,(int)str.length());
}
int n = s.length();
bool dp[n+1];
dp[0] = true;
for(int i = 1; i <= n; ++i)
{
dp[i] = false;
for(int j = 0; j < i; ++j )
{
if(i - j > maxlength) continue;
dp[i] = dp[i] || (dp[j] && set.count(s.substr( j , i - j)));
}
}
return dp[n];
}
};
87. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的
子序列
解法一
动态规划:
- 确定dp数组含义:dp[i]为以第i个元素结尾的最长子序列长度。
- 建立递推公式:dp【i】 = max(dp[i],dp[j] + 1(前提是nums[ j ]小于nums[ i ])),其中 j 为 1 到 i - 1。之所以一直要遍历到头,是因为可能出现123415这种情况,1的递增子序列长度只有1,而比它还前面的4递增子序列更长。
- 边界条件:dp[0] 用不上,dp[1] = 1(以自己为整个序列),后续dp至少为1,因为都可以至少以第i个元素作为整个序列;
- 遍历顺序:从数组首元素遍历到最后一个元素;
/*原版,数组下标未优化
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
int dp[n+1];
if(n == 1) return 1;
dp[0] = 0;
dp[1] = 1;
int maxlen = 0;
for(int i = 2; i <= n; ++i)
{
dp[i] = 1;
for(int j = 1; j < i; ++j)
{
if(nums[i - 1] > nums[j - 1])
{
dp[i] = max(dp[i],dp[j] + 1);
}
}
maxlen = max(dp[i],maxlen);
}
return maxlen;
}
};
*/
//数组下标优化后
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
int dp[n];
if(n == 1) return 1;
int maxlen = 0;
for(int i = 0; i < n; ++i)
{
dp[i] = 1;
for(int j = 0; j < i; ++j)
{
if(nums[i ] > nums[j])
{
dp[i] = max(dp[i],dp[j] + 1);
}
}
maxlen = max(dp[i],maxlen);
}
return maxlen;
}
};
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
int result = 0;
for(int i = 0; i < n; i++){
dp[i] = 1;
for(int j = i-1; j >= 0; j--){
if(nums[j] < nums[i]){
dp[i] = Math.max(dp[i],dp[j] + 1);
}
}
result = Math.max(result,dp[i]);
}
return result;
}
}
88. 乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续
子数组
(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
解法一
动态规划:
-
确定dp数组含义:dp[i]为以第i+1个元素结尾的最大子数组乘积。
-
建立递推公式:通过找规律我们可以发现递推关系dp【i】 = max(nums[ i ] , dp[ i -1 ] * nums[ i ]),即选择继续衔接上一个元素结尾的子数组,或者仅选择该元素,想通过前面最优结果推出后续的最优结果。然而这有一个漏洞,当存在多个负数时,有可能前面乘积最小的含负数组合最后会乘以另外一个负数,获得更大的正数。例如【-4,5,-6】,最大为三者相乘。为了弥补这个漏洞,可以在每轮获取最大值的同时,也用另外一个dp数组,记录最小能小到多少,每次还需要多尝试当前元素乘以之前最小的组合,看是否会产生更大的数或更小的数。
maxdp【i】 = max(nums[ i ] , maxdp[ i -1 ] * nums[ i ] , mindp[ i -1 ] * nums[ i ]);
mindp【i】 = min(nums[ i ] , maxdp[ i -1 ] * nums[ i ] , mindp[ i -1 ] * nums[ i ]); -
边界条件:dp[0] 用不上,dp[1] = 1(以自己为整个序列),后续dp至少为1,因为都可以至少以第i个元素作为整个序列;
-
遍历顺序:从数组首元素遍历到最后一个元素;
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n = nums.size();
int maxplus = nums[0];
vector<int> maxDP(nums),minDP(nums);
for(int i = 1; i < n; ++i)
{
maxDP[i] = max(nums[i],max(maxDP[i-1] * nums[i],minDP[i-1] * nums[i]));
minDP[i] = min(nums[i], min(minDP[i-1] * nums[i],maxDP[i-1] * nums[i]));
maxplus =max(maxDP[i],maxplus);
}
return maxplus;
}
};
89. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
解法一
该问题可以转换为0-1背包问题,将每个元素看作是物品,元素和为背包容量,每次可以考虑放入该元素和不放入该元素,总共有2的n次方种可能。可以采用递归回溯算法,遍历每一种可能的情况,看其总和值是否会等于所有元素总和的一半。但是这种算法时间复杂度太高,超时。
解法 二
0-1背包动态规划:需要用到二维dp[i][j]数组,其中j表示背包容量(可以将本题两个子集和相等转换为有一个子集和到达一半,因此本题可将背包最大容量定为元素总和的一半),i表示物品(有重量和收益,本题重量和收益都是元素值,即weight[ i ] = nums[ i ]
- 确定dp数组含义:dp[i][j]为当背包容量为 j 时,从第1件物品到第i件物品中选取若干件放入背包中,此时背包的最大收益。
- 建立递推公式:当背包容量确定为 j 时,遇到新增物品 i ,我们有两种选择“选取第i件物品,但是需要满足重量不超过j , ”,“不选取第i件物品 , dp[ i - 1] [ j ] ”,则递推公式为dp[ i ][ j ] = max(dp[ i - 1] [ j ] , dp[ i - 1 ][ j - weight[ i ] ] + nums[ i ]);
- 边界条件:初始化二维dp数组时,需要根据递推公式并结合题意初始化。从递推公式可以得出,dp[ i ][ j ]只与第i-1行元素,以及第 j 列 之前的元素有关,因此我们需要初始化第一行和第一列。第一列 j = 0表示背包容量为0,此时最大收益dp[ i ][ 0 ]也为0。第一行 i = 1表示可选物品只有第一件,则此时最大收益需要结合容量判断,容量小于第一件物品时,dp[ 0 ][ j ] 初始化为0;容量大于等于第一件物品时,dp[ 0 ][ j ] = nums[ i ]。
- 遍历顺序:先遍历容量后遍历物品,或者相反均可,因为递推公式均满足(计算dp[ i ][ j ] 只需要dp数组在i j左上方数据存在即可);
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
int sum = accumulate(nums.begin(),nums.end(),0);
if(sum % 2 != 0) return false;
int target = sum/2;
vector<vector<int>> dp(n,vector<int>(target + 1,0));
int maxNum = *max_element(nums.begin(),nums.end());
if(maxNum > target) return false;
for(int i = 0 ; i <= target ; ++i)
{
if(i >= nums[0]) dp[0][i] = nums[0];
}
for(int i = 1 ; i < n; ++i)
{
for(int j = 1 ; j <= target ; ++j)
{
if(nums[i] <= j ) dp[i][j] = max(dp[i-1][j],dp[i-1][j - nums[i]] + nums[i]);
else dp[i][j] = dp[i-1][j];
}
if(dp[i][target] == target) return true;
}
return false;
}
};
解法三
0-1背包动态规划:解法二中dp数组是二维数组,其空间可以优化。从上述递推公式dp[ i ][ j ] = max(dp[ i - 1] [ j ] , dp[ i - 1 ][ j - weight[ i ] ] + nums[ i ])可以看出,第 i 行的结果只与第 i - 1 行的结果相关,因此也可以采用滚动数组的形式将二维压缩至一维dp[ j ]。每次计算第 i 行数据时,dp[ j ]中先存储的是第 i - 1行对应的结果。该过程中有两个关键点:
- 必须先遍历物品:因为压缩至一维dp[ j ]。每次计算第 i 行数据时,dp[ j ]中先存储的是第 i - 1行对应的结果。因此计算 第 i 个物品之前,必须计算出第 i - 1行的所有列的值,即新增一个物品时必须全部计算完所有背包容量的可能;
- 后倒序遍历容量:首先需明确每次计算第 i 行数据时,dp[ j ]中先存储的是第 i - 1行对应的结果。由递推公式可知,计算第i行 第 j 列结果仅与 第i -1 行 第 (小于 j ) 列的结果有关。如果顺序从小到大遍历容量,那么小于 j 的列就会先计算出来,覆盖了上一行保存下来的结果。当后续需要用到前面第i - 1行 某一列的结果时就已经是第 i 行 某一列的结果了。因此为了保证上一行传承下来的值不先被覆盖,倒序遍历容量,此时当用到小于j列的值依然是第 i - 1行留下来的值。
- 边界初始化:在计算第一件物品之前,可以看作没有任何物品待选,因此所有容量下dp均为0。
变成一维dp数组后,行的信息可以删除了,递推公式简写为:dp[ j ] = max(dp[ j ] , dp[ j - weight[ i ] ] + nums[ i ]。
其中,dp[ j ](新一行的值) = max(dp[ j ](旧一行的值) , dp[ j - weight[ i ] ](旧一行的值) + nums[ i ]
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
int sum = accumulate(nums.begin(),nums.end(),0);
if(sum % 2 != 0) return false;
int target = sum/2;
int maxNum = *max_element(nums.begin(),nums.end());
if(maxNum > target) return false;
vector<int> dp(target + 1, 0);
for(int i = 0 ; i < n; ++i)
{
for(int j = target ; j >= 0 ; --j)
{
if(nums[i] <= j ) dp[j] = max(dp[j],dp[j - nums[i]] + nums[i]);
}
if(dp[target] == target) return true;
}
return false;
}
};
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
int n = nums.length;
for(int num : nums){
sum += num;
}
if(sum % 2 != 0) return false;
int target = sum/2;
int[] dp = new int[target+1];
for(int i = 1; i < n; i++){
for(int j = target; j > 0; j--){
if(j >= nums[i]){
dp[j] = Math.max(dp[j],dp[j - nums[i]] + nums[i]);
}
}
}
return dp[target] == target;
}
}
90. 最长有效括号
给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
解法一
动态规划:
- 确定dp数组含义:dp[i]以下标i字符结尾的最长有效括号长度。
- 建立递推公式:由于有效字符一定以‘ ) ’结尾,因此以‘ ( ’结尾的dp肯定为0。可以每次检查两个连续字符,后续需要讨论两种情况,s[ i - 1 ] s[ i ] 为 “))”,“()”。
当“()”出现时,dp[ i ] = dp[ i - 2] + 2;
当“))”出现时,我们需要考虑第一个“)”的dp值是否为非0,即dp[ i - 1 ],表示前面是否有连续相邻有效的字符串,如果无效则dp[ i ] = 0 ;如果有效,则需要看跨越该相邻有效字符串后的第一个字符是否为“(” , 即s[ i - dp[ i - 1 ] - 1] = " ( ",与s[ i ]的“)”相匹配,如果没有,则dp[ i ] = 0;如果有,则组成一个更长的相邻有效子串,并还需要看看更前面有没有连续有效子串,dp[ i ] = dp[i - 1] + dp[ i - dp[ i - 1 ] - 2] + 2; - 边界条件:由于各位置上的最长有效括号长度至少为0,因此可以将dp数组全部初始化为0,或者dp[0] = 0,(仅一个首字符有效长度肯定为0)。
- 遍历顺序:从字符串头遍历到尾,上述需要访问前面的结果或字符,注意数组越界问题;
class Solution {
public:
int longestValidParentheses(string s) {
int length = s.length();
vector<int> dp(length,0);
int maxlength = 0;
for(int i = 1; i < length ; ++i)
{
if(s[i] == ')')
{
if(s[i-1] == '(' )
{
dp[i] = (i >= 2? dp[i-2] : 0) + 2;
}
else if(i - dp[i-1] - 1 >= 0 && s[i - dp[i-1] - 1] == '(' )
{
dp[i] = dp[i-1] + (i - dp[i-1] - 2 >= 0? dp[i - dp[i-1] - 2] : 0) + 2;
}
}
maxlength = max(maxlength,dp[i]);
}
return maxlength;
}
};
91. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
解法一
动态规划:
- 确定dp数组含义:dp[ i ][ j ]表示到达坐标 i ,j位置的路径数量 。
- 建立递推公式:由于只能向下和向右走,则每次有两种方向到达,dp[ i ][ j ] = dp[ i - 1 ][ j ] + dp[ i ][ j - 1 ];
- 边界条件:由递推公式可知,需要初始化第一行和第一列才能依次推出其他位置,到达第一行或第一列某个位置均只有一直向右走或一直向下走一种路径,因此均初始化为1。
- 遍历顺序:先行后列,或者先列后行均可;
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m,vector<int>(n,1));
for(int i = 1; i < m; ++i)
{
for(int j = 1; j < n; ++j)
{
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
解法二
由递推公式可知,dp[ i ][ j ] 仅仅与第i - 1行元素有关( dp[ i ][ j - 1 ]也可由第i - 1行元素结合初始条件依次算出来),因此可以将二维数组压缩至一维数组dp[ j ],每次保留上一行的结果。
递推公式为dp[ j ] = dp j ] + dp[ j - 1 ];
其中dp[ j ](新值) = dp j ](旧值,第i-1行第j列的值) + dp[ j - 1 ](新值)。
依旧需要提前将第一行初始化为1.
由于后续的dp新值需要提前知晓前面的新值,因此遍历顺序依旧是从小到大遍历。
class Solution {
public:
int uniquePaths(int m, int n) {
vector<int> dp(n,1);
for(int i = 1; i < m; ++i)
{
for(int j = 1; j < n; ++j)
{
dp[j] = dp[j] + dp[j-1];
}
}
return dp[n-1];
}
};
92. 最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
解法一
动态规划:
- 确定dp数组含义:dp[ i ][ j ]表示到达坐标 i ,j位置的最小路径和 。
- 建立递推公式:由于只能向下和向右走,则每次有两种方向到达,dp[ i ][ j ] = min(dp[ i - 1 ][ j ] + dp[ i ][ j - 1 ])+ grid[ i ][ j ] ;
- 边界条件:由递推公式可知,需要初始化第一行和第一列才能依次推出其他位置,到达第一行或第一列某个位置均只有一直向右走或一直向下走一种路径,因此均初始化为该行或该列到对应位置的累加和。
- 遍历顺序:先行后列,或者先列后行均可;
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp(m,vector<int>(n,0));
dp[0][0] = grid[0][0];
for(int i = 1; i < n ; ++i)
{
dp[0][i] = dp[0][i-1] + grid[0][i];
}
for(int i = 1; i < m ; ++i)
{
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for(int i = 1; i < m ; ++i)
{
for(int j = 1; j < n ; ++j)
{
dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + grid[i][j];
}
}
return dp[m-1][n-1];
}
};
解法二
由递推公式可知,仅与第 i -1行元素值有关(第i行元素也是由第i-1行元素计算得出),因此可以用滚动数组的方式压缩dp数组至一维。
- 确定dp数组含义:dp[ j ]表示某一行到达第 j 位置的最小路径和 。
- 建立递推公式:由于只能向下和向右走,则每次有两种方向到达,dp[ j ] = min(dp[ j ] + dp[ j - 1 ])+ grid[ i ][ j ] ; 其中dp[ j ](新值)= min(dp[ j ](旧值) + dp[ j - 1 ](新值))+ grid[ i ][ j ]。
- 边界条件:初始化第一行对应位置的累加和,此外,每次第0列元素应该单独考虑为 dp[ j ] = dp[ j ] + grid[ i ][ j ]
- 遍历顺序:先行后列,列从小到大(因为第j列需要第j - 1列的值);
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<int> dp(n,0);
dp[0] = grid[0][0];
for(int i = 1; i < n ; ++i)
{
dp[i] = dp[i-1] + grid[0][i];
}
for(int i = 1; i < m ; ++i)
{
dp[0] += grid[i][0];
for(int j = 1; j < n ; ++j)
{
dp[j] = min(dp[j],dp[j-1]) + grid[i][j];
}
}
return dp[n-1];
}
};
93. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
解法一
动态规划:
- 确定dp数组含义:dp[ i ][ j ]表示到达i 到 j的字符串是否为回文字符串。
- 建立递推公式:如果一个 s【i … j】 字符串符是回文字符串,那么其左右各删除一个字符,剩余的子串依然应该是回文字符串,那么满足递推公式 dp[ i ][ j ] = dp[ i - 1 ][ j - 1 ] && s[ i ] = s[ j ] ;
- 边界条件:最好画图理解!!!通过将dp数组画图可知,正斜对角线的值应该是true,表示单个字符一定是回文字符串。结合递推公式,i 永远小于等于 j,我们可以发现只需要填满dp数组的上三角区域,且区域内的元素均可以从正对角线上的值计算得到。且发现,如果i j差为1,则说明字符串经由两个字符组成,此时 dp[ i ][ j ]应该为判断两个字符是否相等。当如果i j差为2,则说明字符串由三个字符组成,中间的一个字符肯定为回文串,那么也只用此时 dp[ i ][ j ]应该为判断边缘两个字符是否相等即可。
- 遍历顺序:遍历上三角,从下往上遍历,从正斜对角线开始从左向右遍历,这样才能用到之前计算出来的值,建议画图理解;
class Solution {
public:
string longestPalindrome(string s) {
int len = s.length();
int begin = 0,maxlen = 1;
if(len <= 1) return s;
vector<vector<int>> dp(len,vector<int>(len,1));
// for(int i = 0; i < len; ++i)
// {
// dp[i][i] = true;
// }
for(int i = len - 2 ; i >= 0 ; --i )
{
for(int j = i + 1 ; j < len; ++j)
{
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];
}
if(dp[i][j] && maxlen < j - i + 1 )
{
maxlen = j - i + 1;
begin = i;
}
}
}
return s.substr(begin, maxlen);
}
};
解法二
中心扩展法: 通过解法一的启发,如果一个字符串为回文串,那么可以以其为中心向两边同步扩展至下个扩展字符串已经不是回文串停止,记录此时字符串的位置和长度。因此可以遍历每个字符,以其为中心,都可以进行这样的向两边扩展;此外还有第二种情况就是以每两个相邻字符为中心向两边扩展。遍历后取出最长的回文字符串即可。
class Solution {
public:
pair<int, int> expandAroundCenter(const string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
--left;
++right;
}
return {left + 1, right - 1};
}
string longestPalindrome(string s) {
int start = 0, end = 0;
for (int i = 0; i < s.size(); ++i) {
auto [left1, right1] = expandAroundCenter(s, i, i);
auto [left2, right2] = expandAroundCenter(s, i, i + 1);
if (right1 - left1 > end - start) {
start = left1;
end = right1;
}
if (right2 - left2 > end - start) {
start = left2;
end = right2;
}
}
return s.substr(start, end - start + 1);
}
};
94. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
解法一
动态规划:
-
确定dp数组含义:dp[ i ][ j ]表示text1中0-i的字符串,与text2中0-j的字符串最大公共子序列长度。
-
建立递推公式:
设序列X={x1,x2,…,xm}和Y={y1,y2,…,yn}的最长公共子序列为Z={z1,z2,…,zk} ,则
(1)若xm=yn,则zk=xm=yn,且z1,z2,…, zk-1是否为x1,x2,…,xm-1和y1,y2,…,yn-1的最长公共子序列.
(2)若xm≠yn且zk≠xm,则Z是x1,x2,…,xm-1和Y的最长公共子序列.
(3)若xm≠yn且zk≠yn,则Z是X和y1,y2,…,yn-1的最长公共子序列.通俗讲:若当前i,j指向的字符相同,则该字符一定在最长子序列中,指针i,j同时移动,即有:dp[i][j] = dp[i-1][j-1]+1; 若当前i,j指向的字符不相同,则i,j指向的字符至少有一个不在最长子序列中,获取前面子问题的最大解:dp[i][j] = max{dp[i-1][j],dp[i][j-1]}
可得到如下递推公式
-
边界条件:为了简化初始化步骤,我们可以将二维数组增加一个单位,记录两个字符串为空的情况,这样dp数组第0行与第0列均只能为0。
-
遍历顺序:从字符串首元素遍历到尾;
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.length(), n = text2.length();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(int i = 1; i <= m; ++i)
{
for(int j = 1; j <= n; ++j)
{
if(text1[i-1] == text2[j-1])
{
dp[i][j] = dp[i-1][j-1] + 1;
}
else
{
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
};
95. 编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
解法一
动态规划:
-
确定dp数组含义:dp[ i ][ j ]表示word1[0…i - 1] 和 word2[0…j - 1]达到相等的最少操作次数。
-
建立递推公式:如果末尾两个字符相同,那么无需进行任何操作,满足递推公式 dp[ i ][ j ] = dp[ i - 1 ][ j - 1 ]。 如果末尾两个字符不同,可以考虑三种操作:删除串1的末尾字符,则操作次数为dp[ i - 1 ][ j ] + 1;给串2的末尾添加一个字符与串1末尾相同的字符(例如,ab与a,删除ab中的b,或者给a添加一个b,都能使两者相等,且编辑次数均为1)或者在串1末尾添加一个字符与串2末尾相同,则操作次数为dp[ i ][ j - 1 ] + 1;将串1末尾元素替换成串2末尾元素,则操作次数为dp[ i - 1 ][ j - 1 ] + 1。最终需要取三种操作下的最小值。
-
边界条件:为了简化初始化步骤,我们可以将二维数组增加一个单位,记录两个字符串有一个为空的情况,这样dp数组第0行与第0列均只能为按行数列数递增。
-
遍历顺序:从字符串头遍历到尾;
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.length(),n = word2.length();
vector<vector<int>> dp(m+1,vector<int>(n+1));
if(n*m == 0) return n+m;//若有一个字符串为空
for(int i = 0 ; i <= m ; ++i)
{
dp[i][0] = i;
}
for(int i = 0 ; i <= n ; ++i)
{
dp[0][i] = i;
}
for(int i = 1 ; i <= m ; ++i)
{
for(int j = 1 ; j <= n ; ++j)
{
if(word1[i-1] == word2[j-1])
{
dp[i][j] = dp[i-1][j-1];
}
else
{
dp[i][j] = min(dp[i-1][j-1]+1,min(dp[i-1][j]+1 ,dp[i][j-1]+1 ));
}
}
}
return dp[m][n];
}
};
96. 只出现一次的数字
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
解法一
原本可以用哈希表存储元素,记录次数,然后再遍历次数为一的元素,但是该方法空间复杂度为O(n)。可以采用异或处理:
- 任何数和 0 做异或运算,结果仍然是原来的数;
- 任何数和其自身做异或运算,结果是 0;
- 异或运算满足交换律和结合律。
因此可以依次将每个元素进行累计异或处理,遇到一样的就会变为0,只出现一次则将会与0异或得到自己本身。
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (auto e: nums) ret ^= e;
return ret;
}
};
97. 多数元素
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
解法一
可以遍历数组,用哈希表存储数字出现次数,之后遍历哈希表,取次数最大对应的数即可。
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int, int> counts;
int majority = 0, cnt = 0;
for (int num: nums) {
++counts[num];
if (counts[num] > cnt) {
majority = num;
cnt = counts[num];
}
}
return majority;
}
};
解法二
将数组排序,由于多数数超过一半,则中间的数必定是众数
class Solution {
public:
int majorityElement(vector<int>& nums) {
sort(nums.begin(), nums.end());
return nums[nums.size() / 2];
}
};
解法三
由于多数数(众数)的数量超过总数的一半,如果我们一直让两个不同的数相互抵消,最差的情况下就是每次抵消都会消耗一个众数,那么最终留下的数依然是众数。其他情况下不是每次抵消都会消耗众数,可能会消耗其他不同的数,最终留下的必然也是众数。
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate = -1;
int count = 0;
for (int num : nums) {
if(num == candidate)
{
++count;
}
else if( --count < 0)
{
candidate = num;
count = 1;
}
}
return candidate;
}
};
98. 颜色分类
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
进阶:
你能想出一个仅使用常数空间的一趟扫描算法吗?
解法一
可以使用冒泡排序的方法
解法二
双指针:两个指针p0和p2分别指向头和尾,分别用于记录头开始不是0的元素位置,和尾开始不是2的元素位置。遍历元素,如果元素为0,则和p0元素交换,p0++,如果元素为2,则和p2元素交换,p2–,保证0全部放在开头,2全部放在末尾。需要注意的是,若与p2换回的元素是0,那么还需要和p0再次交换一下,不能直接++;若与p2换回的元素还是2,那么还需要继续与–后的p2继续换。因此,如果与p2交换,还需判断交换后的数是不是2或0,如果是则需要再次交换。
class Solution {
public:
void sortColors(vector<int>& nums) {
int p0 = 0, p2 = nums.size() - 1;
int tmp;
for(int j = 0 ;j <= p2 ; ++j)
{
if(nums[j] == 0)
{
nums[j] = nums[p0];
nums[p0] = 0;
++p0;
}
if(nums[j] == 2)
{
nums[j] = nums[p2];
nums[p2] = 2;
--p2;
if(nums[j] != 1)
{
--j;
}
}
}
}
};
99. 下一个排列
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
字典序就类似于对比字符串大小一样,123<132<213<231<312<321
解法一
通过数组字典序排列,我们可以找到规律,从右往左找到第一个左边数大于右边数的情况,左边数就是要被替换的数,且替换为其右边的所有数中刚刚比它大的数,之后再对右边的数进行升序排列,即可得到下一个字典序排列。例如536421,其中左边数为3,右边数为6,寻找6421中刚刚比3大的数为4,与其交换,变为546321,再将6321升序排列,则得到下一个排列541236。
由于找到的左边数的右边总是均为降序,替换后也为降序,因此排序问题可以变为数组序列反转问题,变为升序。
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int n = nums.size();
int lastemlemt = n-1;
for(int i = n-1; i >=0 ; --i)
{
if(nums[i] < nums[lastemlemt])
{
for(int j = n - 1 ; j > i; --j)
{
if(nums[i] < nums[j])
{
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
break;
}
}
sort(nums.begin()+lastemlemt,nums.end());
break;
}
lastemlemt = i;
}
if(lastemlemt == 0)
{
sort(nums.begin(),nums.end());
}
}
};
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int n = nums.size();
int lastemlemt = n-1;
for(int i = n-1; i >=0 ; --i)
{
if(nums[i] < nums[lastemlemt])
{
for(int j = n - 1 ; j > i; --j)
{
if(nums[i] < nums[j])
{
swap(nums[i],nums[j]);
break;
}
}
break;
}
lastemlemt = i;
}
reverse(nums.begin()+lastemlemt,nums.end());
}
};
100. 寻找重复数
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
解法一
如果可以修改数组,可以采用先排序,再找相邻的两个相等的元素。
class Solution {
public:
int findDuplicate(vector<int>& nums) {
sort(nums.begin(),nums.end());
for(int i = 1; i < nums.size(); ++i)
{
if(nums[i-1] == nums[i]) return nums[i];
}
return 0;
}
};
解法二
如果可以修改数组,可以一直交换nums[0]的元素和nums[nums[0]]的元素,依次会将对应的值放在值对应下标的位置,由于数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。那么最终将会往一个已经存在下标对应的数的位置再次放置相同的元素,此时找到重复元素。
例如:
3 1 3 4 2
4 1 3 3 2
2 1 3 3 4
3 1 2 3 4
最终找到重复元素3
class Solution {
public:
int findDuplicate(vector<int>& nums) {
while(1) {
if(nums[0] == nums[nums[0]]) {
break;
}
swap(nums[0], nums[nums[0]]);
}
return nums[0];
}
};
解法三
由于数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。我们可以将整个数组看作是图的一直表示,其中元素下标为节点,元素值对应下一个指向的元素下标。由于存在重复元素,则必出现环。因此,可以用快慢指针的方法,与第26题 环形链表|| 解法相同。原题为找入环点,本体入环点即是重复数字索引。
此题还需注意,除了重复数字产生的环,可能还有其他环存在。例如21224,其中1,4组成独立环,但是我们是从第0个元素为起点,不会进入此种没有重复元素的环。反证法,如果有途径进入非重复元素独立组成的环,那么必有一个节点除了与独立环中的节点相连,还必须与外界相连使其能够进入,这样该节点的元素就重复了,与条件不符,因此不会进入没有重复元素的独立环中。
如果起点元素在非重复元素独立环中,那么必须有元素值为0才行,然后题目规定“数字都在 [1, n] 范围内”,因此不存在起点元素的环。
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int fast = 0,slow = 0;
do{
fast = nums[nums[fast]];//fast指针往前走两步
slow = nums[slow];//slow指针往前走一步
}while(fast != slow);
fast = 0;
while(fast != slow)
{
slow = nums[slow];//slow指针往前走一步
fast = nums[fast];
}
return slow;
}
};