算法学习笔记
分治:
LeetCode #169 求众数
问题特征:
1、分解出的子问题的解可合并为当前解
2、子问题没有公共子问题,若有,可考虑贪心或者动态规划
3、分解出的子问题可解决,且更容易解决!
滑动窗口:
leetCode #209 长度最小子数组
tips:
1、确定前后指针 哪个是基准(在第一层循环里,一步一步加1)
2、确定某个状态前后,前后指针该怎么动,以进入到下一个查找状态
leetCode #1004 最长1子数组
误区:
窗口大小可以变化,并非一直固定大小窗口,往前移动。
tips:
窗口的滑动:
1、重点:题意转换。把「最多可以把 K 个 0 变成 1,求仅包含 1 的最长子数组的长度」转换为 「找出一个最长的子数组,该子数组内最多允许有 K 个 0 」;
2、右指针往右滑动一个,尝试更优解;左指针尝试调整,使得窗口内0的个数小于等于k。
代码如下:
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int left = 0;
int len = k;
int zeroNum = 0;
// 此处直接以right为新的右边界,并判断当前状态进行处理,循环将比较简单
// 如果写while循环,且每次用right+1 判断是否应该向前一步,将使得情况复杂,且需要考虑初始状态。
for (int right = 0; right < nums.size(); right++) {
if (nums[right] == 0) {
zeroNum++;
while (zeroNum > k) {
if (nums[left] == 0) {
zeroNum--;
}
left++;
}
}
len = max(right - left + 1, len);
}
return len;
}
};
滑动窗口模板:
思路:
1、以右指针作为驱动,拖着左指针向前走。右指针每次只移动一步,而左指针在内部 while 循环中每次可能移动多步。右指针是主动前移,探索未知的新区域;左指针是被迫移动,负责寻找满足题意的区间。
2、定义何为满意的区间。
拓扑排序:
leetCode #210 课程表问题
tips:
1、有向图检查是否有环,拓扑排序;无向图检查是否有环,并查集。
2、有BFS 和 DFS两种方式,完成拓扑排序,其中,BFS最为经典,需要记住,且记住需要辅助数据结构,邻接表和队列,每次访问入度为0的点。
3、在拓扑排序过程中,需要使用相同key的map,可使用unordered_multimap,并记住其使用迭代器遍历相同key的方式。
4、BFS检查是否有环,全部访问完成后,查看其拓扑排序的点的个数等于全部即可;DFS判断是否有环,在访问点过程中,因此需要辅助数据结构,以(0,1,2)标记当前点所处的访问状态。
pair<unordered_multimap<int, int>::iterator, unordered_multimap<int, int>::iterator> pos = neb.equal_range(cur);
while (pos.first != pos.second) {
pos.first++;
}
BFS代码如下:
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> res;
// 邻接表
unordered_multimap<int, int> pre;
// 入度数组
vector<int> indegree(numCourses, 0);
// BFS 辅助数据结构,队列
queue<int> traQueue;
for (int i = 0; i < prerequisites.size(); i++) {
indegree[prerequisites[i][0]]++;
pre.insert(make_pair(prerequisites[i][1], prerequisites[i][0]));
}
for (int i = 0; i < numCourses; i++) {
if (indegree[i] == 0) {
traQueue.push(i);
}
}
while (!traQueue.empty()) {
// 遍历相同key,迭代器的使用
pair<unordered_multimap<int, int>::iterator, unordered_multimap<int, int>::iterator> pos =
pre.equal_range(traQueue.front());
// 队列从尾进,从头部出
res.push_back(traQueue.front());
while (pos.first != pos.second) {
indegree[pos.first->second]--;
if (indegree[pos.first->second] == 0) {
// 拓扑排序,BFS方式,必须保证访问的点入度为0
traQueue.push(pos.first->second);
}
pos.first++;
}
traQueue.pop();
}
// 通过判断入度为0的,进入res队列的,是不是等于总的课程数,来确定有没有环
// 如果没有,则入队列的顺序即为一种topo排序
if (res.size() == numCourses) {
return res;
}
return {};
}
};
DFS代码如下:
class Solution {
public:
stack<int> topoSt;
unordered_multimap<int, int> neb;
bool visit(int cur, vector<int> &isVisited) {
// isVisited 标记非常重要,其三种状态,可直接判断未访问,访问中,已访问完,帮助完成拓扑遍历和判断是否成环
// 一个点只要其自身和子节点访问完毕,即可判定访问完,无需考虑其前驱
if (isVisited[cur] == 2) {
return false;
}
if (isVisited[cur] == 1) {
return true;
}
isVisited[cur] = 1;
// 使用equal_range返回迭代器,来遍历相同key
pair<unordered_multimap<int, int>::iterator, unordered_multimap<int, int>::iterator> pos = neb.equal_range(cur);
while (pos.first != pos.second) {
// 继续递归,深度遍历下去
bool isLoop = visit(pos.first->second, isVisited);
if (isLoop) {
return true;
}
pos.first++;
}
isVisited[cur] = 2;
// 未检测到环,则将当前点推入栈中
topoSt.push(cur);
return false;
}
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> res;
if (prerequisites.size() == 0) {
// 对于没有依赖关系,可直接返回一种顺序
for (int i = 0; i < numCourses; i++) {
res.push_back(i);
}
return res;
}
// 对于有重复key情况,使用unordered_multimap
for (int i = 0; i < prerequisites.size(); i++) {
neb.insert(make_pair(prerequisites[i][1], prerequisites[i][0]));
}
bool isLoop;
vector<int> isVisited(numCourses, 0);
// 拓扑排序,从任一点开始即可,因此直接循环开启BFS
for (int i = 0; i < numCourses; i++) {
isLoop = false;
// 深度遍历过程中,直接判断有无成环
isLoop = visit(i, isVisited);
if (isLoop) {
break;
}
}
if (!isLoop) {
// 此时,将栈依次pop出,即为一种topo排序
while (!topoSt.empty()) {
res.push_back(topoSt.top());
topoSt.pop();
}
}
return res;
}
};
差分:
LeetCode #1094
差分思想:
1、源于前缀和;
2、从前往后的一种累加,计算出每一个点上的状态。
3、差分思想的优点在于,每次只用在区间的起点和终点记录下变化,对于区间中的点的状态不用每次都加上。而最后从前往后的累加,即可得出每个点的状态,省去很多区间中的点上的重复计算。
参考博客
https://www.codeleading.com/article/27593349573/
LeetCode #122
心得:
1、差分思想比较明显,有区间,有区间的变化,重点在于捕捉变化;
2、暗含贪心思想,把每一次的增长 纳入囊中;
3、也可以使用动态规划,状态转移为 一天到上一天,而不是一个 持股区间 到上一个 持股区间!!
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<int> change(prices.size(), 0);
for (int i = 1; i < change.size(); i++) {
change[i] = prices[i] - prices[i - 1];
}
int profit = 0;
for (int i = 1; i < change.size(); i++) {
if (change[i] > 0) {
profit += change[i];
}
}
return profit;
}
};
并查集:
LeetCode # 547 求省份数量
问题特征:
1、获取 连通图的个数
tips :
1、find的时候同时进行路径压缩
2、合并的时候注意是 祖先跟祖先挂上
3、三个关键函数,find(), union(), returnNum()
此题典型考察并查集思想,解题主要注意以下几点:
1、压缩路径与建立父子关系同步进行,也就是并非先建立所有联系,再进行压缩路径,而是在建联期间 压缩路径。
2、建联的突破口 从循环处理矩阵中为1的(i,j) 位置开始。
3、建联的形式,是先寻找两个点的祖先节点,如果相等就挂上,如果不等,就在(i,j)中选一个当祖先
LeetCode #684
心得:
1、并查集解法不难看出
2、理解,返回最后一条使其不成树的边,就是第一条使其成环的边
3、简便处理,每个点的祖先初始化成自己,而不是-1;
4、切入点:每加入一条边,就对两个点进行分析,归入各自集团。
class Solution {
public:
int find(vector<int> &parent, int index) {
int cur = index;
while (parent[cur] != cur) {
cur = parent[cur];
}
return cur;
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
vector<int> parent(1001);
for (int i = 0; i < parent.size(); i++) {
parent[i] = i; // 祖先或者代表节点,初始化成自己即可
}
for (int i = 0; i < edges.size(); i++) {
int leftP = find(parent, edges[i][0]); // 每加入一条边,就分析两个节点的祖先,判断关系,并归 类。就是此题切入点
int rightP = find(parent, edges[i][1]);
if (leftP != rightP) {
parent[rightP] = leftP;
} else {
return edges[i]; // 第一条使其成环的边,就是要求的最后一条边
}
}
return {0, 0};
}
};
前缀和:
LeetCode #560
心得:
1、此题需看出子数组之和为前后两个前缀和之差,不然容易想到双指针方法,当然,双指针也可以做,但是基于双指针做优化比较难;
2、为了使得边界情况运算成立,需要将坐标0之前的前缀和(也是0),添加进去
3、不需要把所有前缀和算出来之后,再去遍历出结果(这样就又蜕化成了两层循环),而是每算一个都拿去跟前面的前缀和作比较,因此,需要记录之前出现的前缀和及其次数,因此需要用到hash。
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> preArr;
int count = 0;
preArr.insert(make_pair(0, 1));
int sum = 0;
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
if (preArr.find(sum - k) != preArr.end()) {
count += preArr[sum - k];
}
if (preArr.find(sum) != preArr.end()) {
preArr[sum]++;
} else {
preArr[sum] = 1;
}
}
return count;
}
};
LeetCode #974
心得:
1、与560题相同,为前缀和问题
2、不同的是,560题中,两个前缀和相减即可,此题 中,欲得区间是否能被k整除,要对前后两个前缀和进行模运算,并以hash表存之;
3、当模为负数时,要将其“纠正”;
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
unordered_map<int, int> preArr;
preArr.insert(make_pair(0, 1));
int sum = 0;
int count = 0;
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
int mod = (sum % k + k) % k; // 当模为负数时,应变为正数
if (preArr.find(mod) != preArr.end()) {
count += preArr[mod]; // 同模的前缀和之差 即可被k整除
preArr[mod]++;
} else {
preArr[mod] = 1;
}
}
return count;
}
};
单调栈:
LeetCode #84求最大矩形面积
问题特征:1、空间换时间
2、“在一维数组空间中,找第一个符合某条件的元素”,比如在LeetCode #84 题(求最大矩形面积) 中,要想求出当前柱形高能圈出的最大矩形面积,一个非常关键的点就是要找到数组右边第一个比它低的柱形。
3、在计算结果时,出现了后遍历到,但是先计算出所需结果的情况(先进后出)的情况。
LeetCode #739
1、寻找下一个较大的数,想到单调栈思想
2、循环数组的遍历空值,循环2 * n -1边;
3、while循环中, index.empty()的判断放在前面,避免另一句访问越界
4、pop完,赋值完之后,记得将当前较大数 push进去。
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
vector<int> res(temperatures.size(), 0);
stack<int> index;
index.push(0);
for (int i = 1; i < temperatures.size(); i++) {
if (temperatures[i] <= temperatures[index.top()]) {
index.push(i);
} else {
while (!index.empty() && temperatures[index.top()] < temperatures[i]) {
res[index.top()] = i - index.top();
index.pop();
}
index.push(i);
}
}
return res;
}
};
贪心:
leetcode#452 最少射箭次数
弄清为甚按右端排序:
1、循环是从左往右寻找;
2、从场景分析,将箭放在当前气球区间的最右端,既能保证扎破当前气球,又可以尽最大可能扎破更多的气球;
3、注意,自定义cmp函数如果放在solution类中,应申明为static,因为我们普通的成员函数都有一个隐含的this指针,表面上看我们的谓词函数cmp()只有两个参数,但实际上它有三个参数,而我们调用sort()排序函数的时候只需要用到两个参数进行比较,所以就出现了形参与实参不匹配的情况(函数有三个形参,但是只输入了两个实参)。所以,解决办法就是把谓词函数com()定义为static成员函数。
代码如下:
bool cmp(const vector<int> &a, const vector<int> &b) {
return a[1] < b[1];
}
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(), points.end(), cmp);
int rightIdx = points[0][1];
int shots = 1;
for (int i = 1; i < points.size(); i++) {
if (points[i][0] > rightIdx) {
shots++;
rightIdx = points[i][1];
}
}
return shots;
}
};
前缀树(字典树):
leetcode#208 实现Trie
tips:
1、根据指针是否为空来判断节点内容,也就是说子节点的内容由父节点的指针数组对应元素是否为空决定;
2、无需重复创建节点;
3、查找和 查找前缀可以抽函数;
class Trie {
private:
bool isEnd;
Trie *next[26];
public:
Trie() {
isEnd = false;
memset(next, 0x00, sizeof(next));
}
// 关键抽函数,先找到对应node再说
Trie* searchPrefix(string prefix) {
Trie* node = this;
for (char ch : prefix) {
ch -= 'a';
if (node->next[ch] == nullptr) {
return nullptr;
}
node = node->next[ch];
}
return node;
}
void insert(string word) {
if (word.empty()) {
return;
}
Trie *Cur = this;
for (int i = 0; i < word.size(); i++) {
// 如果已经有此节点,无需重复创建
if (Cur->next[word[i] - 'a'] == nullptr) {
Cur->next[word[i] - 'a'] = new Trie();
}
if (i == word.size() - 1) {
Cur->next[word[i] - 'a']->isEnd = true;
return;
}
Cur = Cur->next[word[i] - 'a'];
}
return;
}
bool search(string word) {
Trie *node = this->searchPrefix(word);
return node != nullptr && node->isEnd == true;
}
bool startsWith(string prefix) {
return this->searchPrefix(prefix) != nullptr;
}
};
BFS :
leetcode#130 被包围的区域
1、连通问题,思考能否用并查集解决
2、解答之初,曾尝试 使用BFS + DFS ,导致思路混乱,以后慎用
3、并查集做法 要重点记住 union的方法,find中的路径压缩方法;
BFS方法
class Solution {
public:
// 注意此处 二维数组申明初始化方式
int dir[4][2] = {{1, 0}, {0, -1}, {-1, 0}, {0, 1}};
void BFSFunc(vector<vector<char>>& board, int xIdx, int yIdx) {
int rowSize = board.size();
int column = board[0].size();
queue<pair<int, int>> traQue;
traQue.push(make_pair(xIdx, yIdx));
while (!traQue.empty()) {
pair<int, int> traNode = traQue.front();
int traX = traNode.first;
int traY = traNode.second;
for (int i = 0; i < 4; i++) {
// 注意此处,坐标转移方法
int nextX = traX + dir[i][0];
int nextY = traY + dir[i][1];
if (nextX >= 0 && nextX < rowSize && nextY >= 0 && nextY < column && board[nextX][nextY] == 'O') {
board[nextX][nextY] = '#';
traQue.push(make_pair(nextX, nextY));
}
}
traQue.pop();
}
}
void solve(vector<vector<char>>& board) {
int rowSize = board.size();
int column = board[0].size();
for (int i = 0; i < rowSize; i++) {
for (int j = 0; j < column; j++) {
if ((i == 0 || i == rowSize - 1 || j == 0 || j == column - 1) && board[i][j] == 'O') {
board[i][j] = '#';
BFSFunc(board, i, j);
}
}
}
for (int i = 0; i < rowSize; i++) {
for (int j = 0; j < column; j++) {
if (board[i][j] == 'O') {
board[i][j] = 'X';
}
if (board[i][j] == '#') {
board[i][j] = 'O';
}
}
}
}
};
并查集方法:
class Solution {
public:
// 注意此处 二维数组申明初始化方式
int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
vector<int> parent;
int virtualNode;
int toIdx(vector<vector<char>>& board, int x, int y) {
return board[0].size() * x + y;
}
void unionElement(int left, int right)
{
// 按条件进行union
int leftRoot = find(left);
int rightRoot = find(right);
if (leftRoot == virtualNode) {
parent[rightRoot] = leftRoot;
return;
}
if (rightRoot == virtualNode) {
parent[leftRoot] = rightRoot;
return;
}
parent[leftRoot] = rightRoot;
}
int find(int node)
{
int tmpRoot = parent[node];
while (parent[tmpRoot] != tmpRoot)
{
tmpRoot = parent[tmpRoot];
}
//路径压缩,重点记住
int next;
while(node != parent[node]){
next = parent[node];
parent[node] = tmpRoot;
node = next;
}
return tmpRoot;
}
bool isConnected(int left, int right)
{
int leftRoot = find(left);
int rightRoot = find(right);
if (leftRoot == rightRoot) {
return true;
}
return false;
}
void solve(vector<vector<char>>& board) {
for (int i = 0; i < board.size() * board[0].size() + 1; i++) {
parent.push_back(i);
}
virtualNode = board.size() * board[0].size();
for (int i = 0; i < board.size(); i++) {
for (int j = 0; j < board[0].size(); ++j) {
if (board[i][j] == 'O') {
int idx = toIdx(board, i, j);
if (i == 0 || i == board.size() - 1 || j == 0 || j == board[0].size() - 1) {
// 边界上的'O'直接跟虚拟点 连通
unionElement(idx, virtualNode);
} else {
for (int p = 0; p < 4; p++) {
int nextX = i + dir[p][0];
int nextY = j + dir[p][1];
if (nextX >= board.size() || nextX < 0 || nextY >= board[0].size() || nextY < 0) {
continue;
}
if (board[nextX][nextY] == 'O') {
int nextIdx = toIdx(board, nextX, nextY);
unionElement(idx, nextIdx);
}
}
}
}
}
}
for (int i = 0; i < board.size(); i++) {
for (int j = 0; j < board[0].size(); j++) {
int node = toIdx(board, i, j);
if (board[i][j] == 'O' && !isConnected(node, virtualNode)) {
board[i][j] = 'X';
}
}
}
}
};
BFS & DFS:
leetcode#934 最短的桥
1、矩阵问题,如果允许,可以修改元素的值,以达到标记和计算的作用;
2、岛屿问题,可观察题意,此处也可以用并查集,但是比较麻烦;
3、此题先找到第一个岛屿每个元素,再“扩散”至第二岛屿,思路比较简单。
class Solution {
public:
int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
queue<pair<int, int>> firstBridge;
void DFSBridge(vector<vector<int>>& grid, int x, int y) {
firstBridge.push(make_pair(x, y));
// 将第一岛屿的元素都改为2,以示标记
grid[x][y] = 2;
for (int i = 0; i < 4; i++) {
int nextX = x + dir[i][0];
int nextY = y + dir[i][1];
if (nextX < 0 || nextX >= grid.size() || nextY >= grid[0].size() || nextY < 0) {
continue;
}
if (grid[nextX][nextY] == 1) {
DFSBridge(grid, nextX, nextY);
}
}
}
int shortestBridge(vector<vector<int>>& grid) {
bool out = false;
// 要跳出两层循环,需如此break
for (int i = 0; i < grid.size(); i++) {
if (out) {
break;
}
for (int j = 0; j < grid[0].size(); j++) {
if (grid[i][j] == 1) {
// 先找到第一岛屿,并深度遍历完
DFSBridge(grid, i, j);
out = true;
break;
}
}
}
// 扩散到第二岛屿,因此,使用宽度优先遍历
while (!firstBridge.empty()) {
int x = firstBridge.front().first;
int y = firstBridge.front().second;
firstBridge.pop();
for (int i = 0; i < 4; i++) {
int nextX = x + dir[i][0];
int nextY = y + dir[i][1];
if (nextX < 0 || nextX >= grid.size() || nextY >= grid[0].size() || nextY < 0 || grid[nextX][nextY] >= 2) {
continue;
}
if (grid[nextX][nextY] == 1) {
if (grid[x][y] == 2) {
return 0;
} else {
return grid[x][y] - 2;
}
}
// 将非岛屿改为递增数字,一则标识,二则记录距离
if (grid[nextX][nextY] == 0) {
grid[nextX][nextY] = grid[x][y] + 1;
firstBridge.push(make_pair(nextX, nextY));
}
}
}
return 0;
}
};
动态规划:
leetcode#322 零钱兑换
1、判断为典型组合问题,思考能否走动态规划;
2、首先尝试将问题用递归解决,amount 在选择一个coin[ i ]之后,转化为 求amount - coin[ i ]的子问题,最开始曽想第一步就把 算出用多少个 coin[ i ],根本原因是找错了递归的方式。
3、找到递归之后,寻找重复子解,然后自下而上构建状态转移公式;
4、具体而言,此题中需要 注意边界情况:amount = 0 时,返回0,而不是-1,因为,-1代表此步无解,而amount到最后必然后减至0,因此,amount = 0不能为-1;
class Solution {
public:
int *memo;
int coinChange(vector<int>& coins, int amount) {
// 注意此处动态数组的new的方式
// 如果初始化则,memo = new int[amount + 1]{i, i, i, ......};
// 但是这里不是vector, 不能(n, i)的方式初始化
memo = new int[amount + 1]();
for (int i = 0; i < amount + 1; i++) {
memo[i] = -1;
}
memo[0] = 0;
for (int i = 1; i < amount + 1; i++) {
int res = INT32_MAX;
for (int j = 0; j < coins.size(); j++) {
if (i - coins[j] < 0) {
continue;
}
if (memo[i - coins[j]] != -1) {
res = min(res, memo[i - coins[j]]);
}
}
if (res != INT32_MAX) {
memo[i] = res + 1;
}
}
return memo[amount];
}
};
leetcode#213 打家劫舍
总结主要有两点:
1、状态转移的步长不要太大,比如此题中,仅仅考虑当前 这家“需不需要进去打劫”,即可递归至下一状态(在打劫下一家时,能获得的最大收益);
2、此题有#198 问题变型题,主要难点在于,将头尾特殊情况,分两种情况进行讨论,即可拆解,再取最大值。
3、套路不变,依然是递归->记忆化搜索->动态规划;
class Solution {
public:
int rob(vector<int>& nums) {
vector<int> memo(nums.size(), 0);
memo[0] = nums[0];
if (nums.size() == 1) {
return memo[0];
}
memo[1] = max(nums[0], nums[1]);
if (nums.size() == 2) {
return memo[1];
}
int res1;
// case1:抢了第一家,因此最后一家不抢
for (int i = 2; i < nums.size() - 1; i++) {
memo[i] = max(nums[i] + memo[i - 2], memo[i - 1]);
}
res1 = memo[nums.size() - 2];
// case2:不抢第一家,因此最后一家算进去
memo[1] = nums[1];
memo[2] = max(nums[1], nums[2]);
for (int i = 3; i < nums.size(); i++) {
memo[i] = max(nums[i] + memo[i - 2], memo[i - 1]);
}
int res2 = memo[nums.size() - 1];
return max(res1, res2);
}
};
leetCode#5最长回文子串
1、寻找状态转移公式,与其他简单DP问题不同,此题dp[i][j] 需转移至dp[i + 1][j - 1] 上;
2、编码过程中,需注意for循环中i 和 j 的位置问题,因为 要转移至 dp[i + 1][j - 1]上,因此第j - 1 列的内容必须依据被算出,因此最外层循环应该是j ,即以列为单位来计算。
3、回文天然具有「状态转移」性质:一个回文去掉头尾字符以后,剩下的部分依然是回文。反之,如果一个字符串头尾两个字符都不相等,那么这个字符串一定不是回文。
4、「动态规划」的方法先找递归,而递归从题目的性质着手。
代码如下:
class Solution {
public:
int length = 1;
int leftIdx = 0;
string longestPalindrome(string s) {
int len = s.length();
vector<vector<bool> > isSub(s.length(), vector<bool>(len)); // 注意动态二维数组创建方式
for (int i = 0; i < s.length(); i++) {
isSub[i][i] = true;
}
// 注意for循环顺序,先循环j
for (int j = 1; j < s.length(); j++) {
for (int i = 0; i < j; i++) {
if (s[i] == s[j]) {
if (i + 2 >= j) {
isSub[i][j] = true;
} else {
isSub[i][j] = isSub[i + 1][j - 1];
}
} else {
isSub[i][j] = false;
}
if (isSub[i][j] == true && j - i + 1 > length) {
length = j - i + 1;
leftIdx = i;
}
}
}
return s.substr(leftIdx, length); // 注意用法,起点 + 长度
}
};
leetCode#376 摆动序列
1、 理解题目波动场景的特殊性,从 status【i】 到 status【i - 1】,仅记一个辅助遍历还不够,新增 down【】 和 up【】,并推导出公式;
2、贪心算法的关键在于,为了使得波动最长,应砍掉所有 单调区间的中间元素,继而仅记录波峰和波谷元素,拼接即最长序列;
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if (nums.size() == 1) {
return 1;
}
vector<int> down(nums.size(), 1);
vector<int> up(nums.size(), 1);
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1]) {
up[i] = max(up[i - 1], down[i - 1] + 1);
down[i] = down[i - 1];
}
if (nums[i] < nums[i - 1]) {
up[i] = up[i - 1];
down[i] = max(up[i - 1] + 1, down[i - 1]);
}
if (nums[i] == nums[i - 1]) {
up[i] = up[i - 1];
down[i] = down[i - 1];
}
}
return max(down[nums.size() - 1], up[nums.size() - 1]);
}
};
leetCode#300 最长增长子序列
1、一开始考虑题目问什么,就把什么定义成状态。题目问最长上升子序列的长度,其实可以把「子序列的长度」定义成状态,但是发现「状态转移」不好做。
2、基于「动态规划」的状态设计需要满足「无后效性」的设计思想,同时nums[i + 1] 能否接在 nums[ i ]构成子序列,取决于nums[ i ]在这个序列中,所以最后将状态定义为「以 nums[i] 结尾 的「上升子序列」的长度」。
「无后效性」的设计思想:让不确定的因素确定下来,以保证求解的过程形成一个逻辑上的有向无环图。这题不确定的因素是某个元素是否被选中,而我们设计状态的时候,让 nums[i] 必需被选中,这一点是「让不确定的因素确定下来」,也是这样设计状态的原因。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> length(nums.size(), 1);
int maxLen = 0;
for (int i = 0; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
length[i] = max(length[j] + 1, length[i]);
}
}
maxLen = max(maxLen, length[i]);
}
return maxLen;
}
};
二分查找:
leetcode#81 搜索旋转排序数组 II
1、循环条件:left <= right , 注意 left == right 成立!
2、维护 循环区间不变量 ,保持迭代的正确性,即 left ,right 移动之后,绝对不能把可能 的 target 漏到区间外面去了;
3、防止 while 死循环, 这里 left 和 right 至少要有一个 赋值为 mid + 1 或者 mid -1, 区间长度每次至少减少1;
最好写左闭右开的二分算法(维护的 是[left, right) 区间,因此没找到最终返回的也应该是left):
while (left < right) {
mid = left + ((right - left) >> 1);
if(target < nums.at(mid)) //如果小于num[mid],那么target在区间[left,mid)之间,mid不可能但依然为边界,为开区间
right = mid;
else if (target > nums.at(mid)) //如果大于num[mid],那么target在区间[mid+1,right)之间
left = mid + 1;
else //如果等于num[mid],那么直接返回
return mid;
}
return left;
本题解法:
class Solution {
public:
bool search(vector<int>& nums, int target) {
int gap = 0;
int left = 0;
int right = nums.size() - 1;
// “回退” 右坐标,使得后部分严格小于前部分
while (nums[right] == nums[left] && right > left) {
right--;
}
// 若元素均相等,可直接判断
if (left == right) {
if (nums[left] == target) {
return true;
} else {
return false;
}
}
int midIdx = 0;
// 找到旋转点所在,注意left == right 仍然满足循环条件
while (left <= right) {
// 若发现当前区间已满足非降序排列,则直接以最后坐标为旋转点
if (nums[right] >= nums[0]) {
gap = right;
break;
}
midIdx = left + (right - left) / 2;
if (midIdx + 1 < nums.size() && nums[midIdx] > nums[midIdx + 1]) {
gap = midIdx;
break;
}
if (midIdx - 1 >= 0 && nums[midIdx - 1] > nums[midIdx]) {
gap = midIdx - 1;
break;
}
// 注意此处left,right 赋值方式,此处维持了左闭右闭区间,要至少保证left,right有一个赋值为midIdx + 1 或者 midIdx - 1
if (nums[midIdx] <= nums[right]) {
right = midIdx - 1;
}
if (nums[midIdx] >= nums[left]) {
left = midIdx;
}
}
// 大于最大值,直接返回
if (target > nums[gap]) {
return false;
}
if (target < nums[0]) {
left = gap + 1;
right = nums.size() - 1;
} else {
left = 0;
right = gap;
}
while (left <= right) {
midIdx = left + (right - left) / 2;
if (nums[midIdx] == target) {
return true;
}
// 此处维护的是左闭右闭区间
if (nums[midIdx] > target) {
right = midIdx - 1;
} else {
left = midIdx + 1;
}
}
return false;
}
};
快排:
leetCode#215 数组中的第K个最大元素
1、C++ 随机数写法
2、快排的写法
class Solution {
public:
int findKthLargestInner(vector<int>& nums, int startIdx, int endIdx, int k) {
// tips:C++ 随机数写法
int baseIdx = rand() % (endIdx - startIdx + 1) + startIdx;
swap(nums[startIdx], nums[baseIdx]);
int left = startIdx;
int right = endIdx;
int target = nums[startIdx];
while (left < right) {
// tips: 此处是 <= 号, =基准的数字在任一边都行,不影响基准的位置
// 以左边的元素为基准,因此,必须right先开始动
while (nums[right] <= target && left < right) {
right--;
}
while (nums[left] >= target && left < right) {
left++;
}
// tips: 如果left == right, 无须swap
if (left < right) {
swap(nums[left], nums[right]);
// swap之后,left,right 不必++ --,因为下一次循环自会处理
}
}
// right先动的,left必在左区间内,可swap
swap(nums[left], nums[startIdx]);
if (left + 1 == k) {
return nums[left];
} else if (left + 1 < k) {
return findKthLargestInner(nums, left + 1, endIdx, k);
} else {
return findKthLargestInner(nums, startIdx, left - 1, k);
}
}
int findKthLargest(vector<int>& nums, int k) {
return findKthLargestInner(nums, 0, nums.size() - 1, k);
}
};
小顶堆:
leetCode#215 数组中的第K个最大元素
1、优先队列的声明
2、小顶堆的声明方式,greater
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 升序队列,小顶堆
// greater<int> 是升序排列方式
priority_queue<int, vector<int>, greater<int>> myqueue;
for (int i = 0; i < nums.size(); ++i) {
if (myqueue.size() < k) {
myqueue.emplace(nums[i]);
} else {
if (myqueue.top() < nums[i]) {
myqueue.pop();
myqueue.push(nums[i]);
}
}
}
return myqueue.top();
}
};