堆(Heap or Priority Queue)、栈(Stack)、队列(Queue)、哈希表类(Hashmap、Hashset)
基础知识
各个数据结构的基本原理,增删查改复杂度。
Stack题目
题目:Leetcode 232. Implement Queue using Stacks
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作( push
、 pop
、 peek
、 empty
):
实现MyQueue
类:
void push(int x)
将元素x
推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
说明:
- 你只能使用标准的栈操作。也就是只有
push to top
,peek/pop from top
,size
, 和is empty
操作是合法的。 - 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)模拟一个栈,只要是标准的栈操作。
进阶:
- 你能否实现每个操作均摊时间复杂度为
O(1)
的队列?换句话说,执行n
个操作的总时间复杂度为O(n)
,即使其中一个操作可能花费较长时间。
示例:
输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
题解
class MyQueue {
public:
/** Initialize your data structure here. */
stack<int> stack1;
stack<int> stack2;
MyQueue() {
}
/** Push element x to the back of queue. */
void push(int x) {
stack1.push(x);
}
/** Removes the element from in front of queue and returns that element. */
int pop() {
while (!stack1.empty()) {
stack2.push(stack1.top());
stack1.pop();
}
int num = stack2.top();
stack2.pop();
while (!stack2.empty()) {
stack1.push(stack2.top());
stack2.pop();
}
return num;
}
/** Get the front element. */
int peek() {
while (!stack1.empty()) {
stack2.push(stack1.top());
stack1.pop();
}
int num = stack2.top();
while (!stack2.empty()) {
stack1.push(stack2.top());
stack2.pop();
}
return num;
}
/** Returns whether the queue is empty. */
bool empty() {
return stack1.empty();
}
};
题目:Leetcode 150. Evaluate Reverse Polish Notation
根据 逆波兰表示法,求表达式的值。
有效的算符包括 +
、-
、*
、/
。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
- 整数除法只保留整数部分。
- 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:
该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
提示:
1 <= tokens.length <= 104
tokens[i]
要么是一个算符("+"
、"-"
、"*"
或"/"
),要么是一个在范围[-200, 200]
内的整数
逆波兰表达式:
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
- 平常使用的算式则是一种中缀表达式,如
( 1 + 2 ) * ( 3 + 4 )
。 - 该算式的逆波兰表达式写法为
( ( 1 2 + ) ( 3 4 + ) * )
。
逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成
1 2 + 3 4 + *
也可以依据次序计算出正确结果。 - 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
题解
int atoi(const char *str);
把参数str所指向的字符串转换为一个int型整数
const char *c_str();
返回一个指向正规C字符串的指针常量, 内容与本string串相同。
class Solution {
public:
bool isNumber(string& token) {
if (token == "+" || token == "-" || token == "*" || token == "/") {
return false;
}
return true;
}
int evalRPN(vector<string>& tokens) {
stack<int> stk;
for (int i = 0; i < tokens.size(); i++) {
string token = tokens[i];
if (isNumber(token)) {
// int atoi(const char *str); 把参数str所指向的字符串转换为一个int型整数
// const char *c_str(); 返回一个指向正规C字符串的指针常量, 内容与本string串相同
// 下面两行代码等同于:stk.push(atoi(token.c_str()));
const char *str = &token[0];
stk.push(atoi(str));
} else {
int num1 = stk.top();
stk.pop();
int num2 = stk.top();
stk.pop();
switch (token[0]) {
case '+': stk.push(num1 + num2); break;
case '-': stk.push(num2 - num1); break;
case '*': stk.push(num1 * num2); break;
case '/': stk.push(num2 / num1); break;
}
}
}
return stk.top();
}
};
题目:Leetcode 224. Basic Calculator II (I, II, III, IV)
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
示例 1:
输入:s = "1 + 1"
输出:2
示例 2:
输入:s = " 2-1 + 2 "
输出:3
示例 3:
输入:s = "(1+(4+5+2)-3)+(6+8)"
输出:23
提示:
1 <= s.length <= 3 * 105
s
由数字、'+'
、'-'
、'('
、')'
、和' '
组成s
表示一个有效的表达式
题解
建立两个栈(st_num和st_signs),来存储遇到括号前的符号sign(初始值为1,代表正,-1代表负),和括号外面已经算过的结果ans(初始值值为0),从栈顶到栈底依次是从内层到外层的结果。
扫描字符串,遇到空格直接跳过,遇到正负号则更新正负号,遇到左括号,将当前的结果和符号存入对应的栈中,遇到右括号,更新运算结果。
为什么要这样做?
假设没有括号的情况下,我们可以直接在当前得到的结果上累加。但是有括号就不同了,我们要先把之前算过的结果存起来,然后再单独算计算括号里面的结果,直到遇到反括号,将当前括号内的结果(当前括号内的结果是要包括括号前的符号的)和上一层的结果(上次的ans)加起来,上一层的结果即为st_num的栈顶元素。
作者:maple-z
链接:https://leetcode-cn.com/problems/basic-calculator/solution/tu-jie-shuang-zhan-suan-fa-by-maple-z-qe2x/
class Solution {
public int calculate(String s) {
int ans = 0;
char[] str = s.toCharArray();
int len = str.length;
Stack<Integer> st_num = new Stack<>();
Stack<Integer> st_signs = new Stack<>();
int sign = 1;//正负号,运算符号
for(int i = 0; i<len; i++){
if(str[i] == ' ') {
continue;
}
if(str[i] == '+' || str[i] == '-') {
sign = str[i] == '+' ? 1 : -1;
} else if(str[i] >= '0' && str[i] <= '9') {//数字
int num = str[i] - '0';
while(i < len-1 && str[i+1] >= '0' && str[i+1] <= '9') {//将这个数字找完
num = num * 10 + (str[++i] - '0');
}
ans += sign * num;
}else if(str[i] == '('){//左括号,暂存结果
st_num.push(ans);
st_signs.push(sign);
ans = 0;
sign = 1;
} else {
ans = st_num.pop() + ans * st_signs.pop();//右括号更新结果
}
}
return ans;
}
}
题目:Leetcode 20. Valid Parentheses
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
1、左括号必须用相同类型的右括号闭合。
2、左括号必须以正确的顺序闭合。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([)]"
输出:false
示例 5:
输入:s = "{[]}"
输出:true
题解
class Solution {
public:
bool isValid(string s) {
int n = s.size();
if (n % 2 == 1) {
return false;
}
unordered_map<char, char> pairs = {
{')', '('},
{']', '['},
{'}', '{'}
};
stack<char> stk;
for (char ch: s) {
if (pairs.count(ch)) { // pair中存在key = ch
if (stk.empty() || stk.top() != pairs[ch]) {
return false;
}
stk.pop();
} else {
stk.push(ch); // 后遇到的左括号放入栈顶
}
}
return stk.empty();
}
};
题目:Leetcode 1472. Design Browser History
你有一个只支持单个标签页的浏览器
,最开始你浏览的网页是homepage
,你可以访问其他的网站url
,也可以在浏览历史中后退steps
步或前进steps
步。
请你实现BrowserHistory
类:
-
BrowserHistory(string homepage)
,用homepage
初始化浏览器类。 -
void visit(string url)
从当前页跳转访问url
对应的页面 。执行此操作会把浏览历史前进的记录全部删除。 -
string back(int steps)
在浏览历史中后退steps
步。如果你只能在浏览历史中后退至多x
步且steps
>x
,那么你只后退x
步。请返回后退至多steps
步以后的url
。 -
string forward(int steps)
在浏览历史中前进steps
步。如果你只能在浏览历史中前进至多x
步且steps > x
,那么你只前进x
步。请返回前进至多steps
步以后的url
。
示例:
输入:
["BrowserHistory","visit","visit","visit","back","back","forward","visit","forward","back","back"]
[["leetcode.com"],["google.com"],["facebook.com"],["youtube.com"],[1],[1],[1],["linkedin.com"],[2],[2],[7]]
输出:
[null,null,null,null,"facebook.com","google.com","facebook.com",null,"linkedin.com","google.com","leetcode.com"]
解释:
BrowserHistory browserHistory = new BrowserHistory("leetcode.com");
browserHistory.visit("google.com"); // 你原本在浏览 "leetcode.com" 。访问 "google.com"
browserHistory.visit("facebook.com"); // 你原本在浏览 "google.com" 。访问 "facebook.com"
browserHistory.visit("youtube.com"); // 你原本在浏览 "facebook.com" 。访问 "youtube.com"
browserHistory.back(1); // 你原本在浏览 "youtube.com" ,后退到 "facebook.com" 并返回 "facebook.com"
browserHistory.back(1); // 你原本在浏览 "facebook.com" ,后退到 "google.com" 并返回 "google.com"
browserHistory.forward(1); // 你原本在浏览 "google.com" ,前进到 "facebook.com" 并返回 "facebook.com"
browserHistory.visit("linkedin.com"); // 你原本在浏览 "facebook.com" 。 访问 "linkedin.com"
browserHistory.forward(2); // 你原本在浏览 "linkedin.com" ,你无法前进任何步数。
browserHistory.back(2); // 你原本在浏览 "linkedin.com" ,后退两步依次先到 "facebook.com" ,然后到 "google.com" ,并返回 "google.com"
browserHistory.back(7); // 你原本在浏览 "google.com", 你只能后退一步到 "leetcode.com" ,并返回 "leetcode.com"
题解
使用一个栈记录浏览历史,使用一个 pos 指针记录当前网页在栈中的位置。每次 back 和 forward 操作都只更新 pos 指针。因为visit操作会把浏览历史前进的记录全部删除,所以每次 visit 先根据 pos 更新下栈顶指针,然后再将 url 入栈。
作者:Time-Limit
链接:https://leetcode-cn.com/problems/design-browser-history/solution/sha-dai-ma-bu-dai-ma-de-xian-shang-tu-by-time-limi/
/*
思路:
1.栈只用来做存储
2.给栈定义一个访问指针
3.通过访问指针进行快速跳转和数据删除
*/
class BrowserHistory {
public:
int pos = -1;//当前网页在栈中的位置
int top = 0;//栈顶初始位置
string history[5001];
BrowserHistory(string homepage){//用主页初始化浏览器
visit(homepage);//可以直接调用下边的访问主页
}
void visit(string url) {//跳转访问url界面
pos++;//指针上移
top = pos;//栈顶下移,如果访问的是之前的网页则删除当前网页与目标网页之间的记录
history[top++] = url;//把url界面放在栈顶并把栈顶指针上移
}
string back(int steps) {//后退
if(steps > pos) {//后退步数大于已经浏览过的网页数
steps = pos;//说明只能后退这么多,就只让她后退到首页
}
//后退步数大于已经浏览过的网页数时,pos = pos - steps
pos -= steps;
return history[pos];
}
string forward(int steps) {//前进
steps = min(steps, top - pos - 1);//选择要前进的步数和可以前进的步数两者的最小值
//pos = pos + steps ,pos指向的是现在浏览的网页,加上要前进的步数,就是要返回的网页
pos += steps;
return history[pos];//返回pos所指向的网页
}
};
题目:Leetcode 1209. Remove All Adjacent Duplicates in String II
给你一个字符串s
,「k
倍重复项删除操作」
将会从s
中选择k
个相邻且相等的字母,并删除它们,使被删去的字符串的左侧和右侧连在一起。
你需要对s
重复进行无限次这样的删除操作,直到无法继续为止。
在执行完所有删除操作后,返回最终得到的字符串。
本题答案保证唯一。
示例 1:
输入:s = "abcd", k = 2
输出:"abcd"
解释:没有要删除的内容。
示例 2:
输入:s = "deeedbbcccbdaa", k = 3
输出:"aa"
解释:
先删除 "eee" 和 "ccc",得到 "ddbbbdaa"
再删除 "bbb",得到 "dddaa"
最后删除 "ddd",得到 "aa"
示例 3:
输入:s = "pbbcggttciiippooaais", k = 2
输出:"ps"
题解
方法1:暴力法
- 记录字符串的长度。
- 遍历字符串:
- 如果当前字符与前一个相同,计数器加 1。
- 否则,重置计数器为 1。
- 如果计数器等于
k
,删除这k
个字符。 - 如果字符串的长度被改变,从头开始重新遍历字符串。
- 如果当前字符与前一个相同,计数器加 1。
string removeDuplicates(string s, int k) {
int length = -1;
int count = 1;
while (length != s.size()) { // 字符串s中没有连续k个相同字符时,跳出while循环
length = s.size();
for (int i = 0; i < s.size(); ++i) {
if (i == 0 || s[i] != s[i - 1]) {
count = 1;
} else {
count++;
if (count == k) {
s.erase(i - k + 1, k); // 删除从i-k+1开始的连续k个元
break;
}
}
}
}
return s;
}
方法2:记忆计数
方法1是给可能连续k个相同的首个字符计数,只要s[i - 1] != s[i]
,就重新寻找下一个可能连续k
个相同的首个字符,删除连续k
个相同的字符后需要重新从头遍历。
方法2是给每个字符计数,不必每次删除完字符后从头开始重新为每个字符计数。
- 初始长度为
n
的数组counts
。 - 遍历字符串:
- 如果当前字符与上一个字符相等,令
counts[i] = counts[i - 1] + 1
。- 否则,令
counts[i] = 1
。
- 否则,令
- 如果
counts[i] = k
,删除这k
个字符,令i = i - k
。
- 如果当前字符与上一个字符相等,令
复杂度分析
时间复杂度:O(n)
,其中n
是字符串长度。每个字符只处理一次。
空间复杂度:O(n)
,vector
空间。
string removeDuplicates(string s, int k) {
vector<int> count(s.size());
for (int i = 0; i < s.size(); i++) {
if (i == 0 || s[i] != s[i - 1]) {
count[i] = 1;
} else {
count[i] = count[i - 1] + 1;
if (count[i] == k) {
s.erase(i - k + 1, k); 素
i = i - k;
}
}
}
return s;
}
方法3:栈
当前字符与前一个不同时,往栈中压入 1
。否则栈顶元素加 1
。
复杂度分析
时间复杂度:O(n)
,其中n
是字符串长度。每个字符只处理一次。
空间复杂度:O(n)
,vector
空间。
string removeDuplicates(string s, int k) {
stack<int> stack;
for (int i = 0; i < s.size(); i++) {
if (i == 0 || s[i] != s[i - 1]) {
stack.push(1);
} else {
//int top = stack.top() + 1;
//stack.pop();
//stack.push(top);
stack.top()++;
if (stack.top() == k) {
stack.pop(); // 连续k个相同的字符删除,该字符的计数也删除
s.erase(i - k + 1, k);
i = i - k;
}
}
}
return s;
}
方法4:双指针
该方法由lee215提出,使用双指针可以优化方法二和三中的字符串操作。使用快慢指针复制字符。每次需要删除 k
个元素时,只需要将慢指针回退 k
个位置。
- 初始慢指针 j 等于 0。
- 使用快指针 i 遍历字符串:
- 令 s[i] = s[j]。
- 如果 s[j] = s[j - 1],则栈顶元素加 1。
- 否则,栈中压入 1。
- 如果计数器等于 k,j = j - k,并弹出栈顶元素。
- 返回字符串的前 j 个字符
复杂度分析
时间复杂度:O(n)
,其中n
是字符串长度。每个字符只处理一次。
空间复杂度:O(n)
,vector
空间。
string removeDuplicates(string s, int k) {
auto j = 0; // 慢指针
stack<int> counts;
for (auto i = 0; i < s.size(); i++, j++) {
s[j] = s[i];
if (j == 0 || s[j] != s[j - 1]) {
counts.push(1);
} else {
counts.top()++;
if(counts.top() == k) {
counts.pop();
j = j - k;
}
}
}
return s.substr(0, j);
}
题目:Leetcode 1249. Minimum Remove to Make Valid Parentheses
给你一个由(
、)
和小写字母组成的字符串s
。
你需要从字符串中删除最少数目的(
或者)
(可以删除任意位置的括号),使得剩下的「括号字符串」有效。
请返回任意一个合法字符串。
有效「括号字符串」应当符合以下 任意一条 要求:
- 空字符串或只包含小写字母的字符串
- 可以被写作
AB
(A
连接B
)的字符串,其中A
和B
都是有效「括号字符串」 - 可以被写作
(A)
的字符串,其中A
是一个有效的「括号字符串」
示例 1:
输入:s = "lee(t(c)o)de)"
输出:"lee(t(c)o)de"
解释:"lee(t(co)de)" , "lee(t(c)ode)" 也是一个可行答案。
示例 2:
输入:s = "a)b(c)d"
输出:"ab(c)d"
示例 3:
输入:s = "))(("
输出:""
解释:空字符串也是有效的
示例 4:
输入:s = "(a(b(c)d)"
输出:"a(b(c)d)"
提示:
1 <= s.length <= 10^5
s[i]
可能是'('
、')'
或英文小写字母
题解
目的:将多余的'('
、')'
改为‘ ’
,然后重新拼接。
string minRemoveToMakeValid(string s) {
stack<int> stack;
for (int i = 0; i < s.size(); i++) {
if (s[i] == '(') {
// 遇到'(',就将其索引入栈
stack.push(i);
}
if (s[i] == ')') {
// 将多余的')'改为' ':遇到')'之前还没出现'(' 或是 栈中已经没有与')'配对的'('
if (stack.empty()) {
s[i] = ' ';
} else {
// ')'与'('配对一次,栈顶的'('的索引移除一次
stack.pop();
}
}
}
while (!stack.empty()) { // 将多余的'('改为' '
s[stack.top()] = ' ';
stack.pop();
}
string ans;
for (int i = 0; i < s.size(); i++) {
if (s[i] != ' ') {
ans += s[i];
}
}
return ans;
}
题目:Leetcode 735. Asteroid Collision
给定一个整数数组asteroids
,表示在同一行的行星。
对于数组中的每一个元素,其绝对值表示行星的大小,正负表示行星的移动方向(正表示向右移动,负表示向左移动)。每一颗行星以相同的速度移动。
找出碰撞后剩下的所有行星。碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。
示例 1:
输入:asteroids = [5,10,-5]
输出:[5,10]
解释:10 和 -5 碰撞后只剩下 10 。 5 和 10 永远不会发生碰撞。
示例 2:
输入:asteroids = [8,-8]
输出:[]
解释:8 和 -8 碰撞后,两者都发生爆炸。
示例 3:
输入:asteroids = [10,2,-5]
输出:[10]
解释:2 和 -5 发生碰撞后剩下 -5。10 和 -5 发生碰撞后剩下 10 。
示例 4:
输入:asteroids = [-2,-1,1,2]
输出:[-2,-1,1,2]
解释:-2 和 -1 向左移动,而 1 和 2 向右移动。由于移动方向相同的行星不会发生碰撞,所以最终没有行星发生碰撞。
题解
只有一种情况会发生碰撞:左星大于0,右星小于0。
class Solution {
public:
vector<int> asteroidCollision(vector<int>& asteroids) {
vector<int> res;
res.push_back(asteroids[0]);
for (int i = 1; i < asteroids.size(); i++) {
bool flag = true; // flag为true时asteroids[i]入栈
// 只有栈顶元素为正并且将要入栈的元素为负时才会发生碰撞
while (!res.empty() && flag && res.back() > 0 && asteroids[i] < 0) {
// 绝对值小于栈顶元素:不弹出栈顶元素,asteroids[i]不入栈
if (-asteroids[i] < res.back()) {
flag = false;
}
// 绝对值大于栈顶元素:弹出栈顶元素,继续比较下一个栈顶元素
else if (-asteroids[i] > res.back()) {
res.pop_back();
}
// 绝对值等于栈顶元素:弹出栈顶元素,asteroids[i]不入栈
else if (-asteroids[i] == res.back()) {
res.pop_back();
flag = false;
}
}
if (flag)
res.push_back(asteroids[i]);
}
return res;
}
};
作者:sscicada
链接:https://leetcode-cn.com/problems/asteroid-collision/solution/csi-lu-qing-xi-by-sscicada-ezor/